input: allow dynamic change of midi drum config

This commit is contained in:
Megamouse 2024-06-26 19:29:29 +02:00
parent e790842007
commit 0679b502f2
4 changed files with 139 additions and 119 deletions

View file

@ -9,9 +9,6 @@ using namespace std::chrono_literals;
LOG_CHANNEL(rb3_midi_drums_log);
namespace
{
namespace controller
{
@ -158,67 +155,6 @@ u8 min_velocity()
return g_cfg_rb3drums.minimum_velocity;
}
enum class Id : u8
{
// Each 'Note' can be triggered by multiple different numbers.
// Keeping them flattened in an enum for simplicity / switch statement usage.
// These follow the rockband 3 midi pro adapter support.
Snare0 = 38,
Snare1 = 31,
Snare2 = 34,
Snare3 = 37,
Snare4 = 39,
HiTom0 = 48,
HiTom1 = 50,
LowTom0 = 45,
LowTom1 = 47,
FloorTom0 = 41,
FloorTom1 = 43,
Hihat0 = 22,
Hihat1 = 26,
Hihat2 = 42,
Hihat3 = 54,
Ride0 = 51,
Ride1 = 53,
Ride2 = 56,
Ride3 = 59,
Crash0 = 49,
Crash1 = 52,
Crash2 = 55,
Crash3 = 57,
Kick0 = 33,
Kick1 = 35,
Kick2 = 36,
HihatPedal = 44,
// These are from alesis nitro mesh max. ymmv.
SnareRim = 40, // midi pro adapter counts this as snare.
HihatWithPedalUp = 46, // The midi pro adapter considers this a normal hihat hit.
HihatPedalPartial = 23, // If pedal is not 100% down, this will be sent instead of a normal hihat hit.
// Internal value used for converting midi CC.
// Values past 127 are not used in midi notes.
MidiCC = 255,
};
// Intermediate mapping regardless of which midi ids triggered it.
enum class Note : u8
{
Invalid,
Kick,
HihatPedal,
Snare,
SnareRim,
HiTom,
LowTom,
FloorTom,
HihatWithPedalUp,
Hihat,
Ride,
Crash,
};
Note str_to_note(const std::string_view name)
{
static const std::unordered_map<std::string_view, Note> mapping{
@ -298,27 +234,21 @@ std::unordered_map<Id, Note> create_id_to_note_mapping()
{Id::Crash2, Note::Crash},
{Id::Crash3, Note::Crash},
};
// Apply configured overrides.
auto split = fmt::split(g_cfg_rb3drums.midi_overrides.to_string(), {","});
for (const auto& segment : split)
const std::vector<std::string> segments = fmt::split(g_cfg_rb3drums.midi_overrides.to_string(), {","});
for (const std::string& segment : segments)
{
if (auto midi_override = parse_midi_override(segment))
if (const auto midi_override = parse_midi_override(segment))
{
auto id = midi_override->first;
auto note = midi_override->second;
const auto id = midi_override->first;
const auto note = midi_override->second;
mapping[id] = note;
}
}
return mapping;
}
Note id_to_note(Id id)
{
static const auto mapping = create_id_to_note_mapping();
const auto it = mapping.find(id);
return it != mapping.cend() ? it->second : Note::Invalid;
}
namespace combo
{
@ -345,39 +275,18 @@ std::vector<u8> parse_combo(const std::string_view name, const std::string_view
return notes;
}
struct Definition
{
std::string name;
std::vector<u8> notes;
std::function<rb3drums::KitState()> create_state;
Definition(std::string name, const std::string_view csv, const std::function<rb3drums::KitState()> create_state)
: name{std::move(name)}
, notes{parse_combo(this->name, csv)}
, create_state{create_state}
{}
};
std::chrono::milliseconds window()
{
return std::chrono::milliseconds{g_cfg_rb3drums.combo_window_ms};
}
const std::vector<Definition>& definitions()
{
// Only parse once and cache.
static const std::vector<Definition> defs{
{"start", g_cfg_rb3drums.combo_start.to_string(), []{ return drum::start_state(); }},
{"select", g_cfg_rb3drums.combo_select.to_string(), []{ return drum::select_state(); }},
{"hold kick", g_cfg_rb3drums.combo_toggle_hold_kick.to_string(), []{ return drum::toggle_hold_kick_state(); }}
};
return defs;
}
}
} // namespace midi
namespace
{
void set_flag(u8* buf, [[maybe_unused]] std::string_view name, const controller::FlagByIndex& fbi)
{
auto i = fbi[drum::INDEX];
@ -397,6 +306,12 @@ void set_flag_if_any(u8* buf, std::string_view name, const controller::FlagByInd
}
usb_device_rb3_midi_drums::Definition::Definition(std::string name, const std::string_view csv, const std::function<rb3drums::KitState()> create_state)
: name{std::move(name)}
, notes{midi::combo::parse_combo(this->name, csv)}
, create_state{create_state}
{}
usb_device_rb3_midi_drums::usb_device_rb3_midi_drums(const std::array<u8, 7>& location, const std::string& device_name)
: usb_device_emulated(location)
{
@ -603,6 +518,12 @@ void usb_device_rb3_midi_drums::interrupt_transfer(u32 buf_size, u8* buf, u32 /*
}
memcpy(buf, bytes.data(), bytes.size());
if (g_cfg_rb3drums.reload_requested)
{
m_id_to_note_mapping = midi::create_id_to_note_mapping();
combo.reload_definitions();
}
while (true)
{
u8 midi_msg[32];
@ -712,6 +633,12 @@ rb3drums::KitState usb_device_rb3_midi_drums::parse_midi_message(u8* msg, usz si
return rb3drums::KitState{};
}
midi::Note usb_device_rb3_midi_drums::id_to_note(midi::Id id)
{
const auto it = m_id_to_note_mapping.find(id);
return it != m_id_to_note_mapping.cend() ? it->second : midi::Note::Invalid;
}
rb3drums::KitState usb_device_rb3_midi_drums::parse_midi_note(const u8 id, const u8 velocity)
{
if (velocity < midi::min_velocity())
@ -722,7 +649,7 @@ rb3drums::KitState usb_device_rb3_midi_drums::parse_midi_note(const u8 id, const
rb3drums::KitState kit_state{};
kit_state.expiry = std::chrono::steady_clock::now() + drum::hit_duration();
auto note = midi::id_to_note(static_cast<midi::Id>(id));
const midi::Note note = id_to_note(static_cast<midi::Id>(id));
switch (note)
{
case midi::Note::Kick: kit_state.kick_pedal = velocity; break;
@ -751,7 +678,8 @@ bool usb_device_rb3_midi_drums::is_midi_cc(const u8 id, const u8 value)
{
return false;
}
auto is_past_threshold = [](u8 value)
const auto is_past_threshold = [](u8 value)
{
const u8 threshold = g_cfg_rb3drums.midi_cc_threshold;
return g_cfg_rb3drums.midi_cc_invert_threshold
@ -834,6 +762,15 @@ bool rb3drums::KitState::is_drum() const
return std::max({snare, hi_tom, low_tom, floor_tom}) >= midi::min_velocity();
}
void usb_device_rb3_midi_drums::ComboTracker::reload_definitions()
{
m_definitions = {
{"start", g_cfg_rb3drums.combo_start.to_string(), []{ return drum::start_state(); }},
{"select", g_cfg_rb3drums.combo_select.to_string(), []{ return drum::select_state(); }},
{"hold kick", g_cfg_rb3drums.combo_toggle_hold_kick.to_string(), []{ return drum::toggle_hold_kick_state(); }}
};
}
void usb_device_rb3_midi_drums::ComboTracker::add(u8 note)
{
if (!midi_notes.empty() && std::chrono::steady_clock::now() >= expiry)
@ -843,9 +780,8 @@ void usb_device_rb3_midi_drums::ComboTracker::add(u8 note)
}
const usz i = midi_notes.size();
const auto& defs = midi::combo::definitions();
bool is_in_combo = false;
for (const auto& def : defs)
for (const auto& def : m_definitions)
{
if (i < def.notes.size() && note == def.notes[i])
{
@ -879,7 +815,7 @@ std::optional<rb3drums::KitState> usb_device_rb3_midi_drums::ComboTracker::take_
{
return {};
}
for (const auto& combo : midi::combo::definitions())
for (const auto& combo : m_definitions)
{
if (midi_notes == combo.notes)
{

View file

@ -38,10 +38,83 @@ struct KitState
bool is_drum() const;
};
}; // namespace rb3drums
} // namespace rb3drums
namespace midi
{
enum class Id : u8
{
// Each 'Note' can be triggered by multiple different numbers.
// Keeping them flattened in an enum for simplicity / switch statement usage.
// These follow the rockband 3 midi pro adapter support.
Snare0 = 38,
Snare1 = 31,
Snare2 = 34,
Snare3 = 37,
Snare4 = 39,
HiTom0 = 48,
HiTom1 = 50,
LowTom0 = 45,
LowTom1 = 47,
FloorTom0 = 41,
FloorTom1 = 43,
Hihat0 = 22,
Hihat1 = 26,
Hihat2 = 42,
Hihat3 = 54,
Ride0 = 51,
Ride1 = 53,
Ride2 = 56,
Ride3 = 59,
Crash0 = 49,
Crash1 = 52,
Crash2 = 55,
Crash3 = 57,
Kick0 = 33,
Kick1 = 35,
Kick2 = 36,
HihatPedal = 44,
// These are from alesis nitro mesh max. ymmv.
SnareRim = 40, // midi pro adapter counts this as snare.
HihatWithPedalUp = 46, // The midi pro adapter considers this a normal hihat hit.
HihatPedalPartial = 23, // If pedal is not 100% down, this will be sent instead of a normal hihat hit.
// Internal value used for converting midi CC.
// Values past 127 are not used in midi notes.
MidiCC = 255,
};
// Intermediate mapping regardless of which midi ids triggered it.
enum class Note : u8
{
Invalid,
Kick,
HihatPedal,
Snare,
SnareRim,
HiTom,
LowTom,
FloorTom,
HihatWithPedalUp,
Hihat,
Ride,
Crash,
};
}
class usb_device_rb3_midi_drums : public usb_device_emulated
{
public:
usb_device_rb3_midi_drums(const std::array<u8, 7>& location, const std::string& device_name);
~usb_device_rb3_midi_drums();
void control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) override;
void interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) override;
private:
usz response_pos{};
bool buttons_enabled{};
@ -50,9 +123,19 @@ private:
bool hold_kick{};
bool midi_cc_triggered{};
struct Definition
{
std::string name;
std::vector<u8> notes;
std::function<rb3drums::KitState()> create_state;
Definition(std::string name, const std::string_view csv, const std::function<rb3drums::KitState()> create_state);
};
class ComboTracker
{
public:
void reload_definitions();
void add(u8 note);
void reset();
std::optional<rb3drums::KitState> take_state();
@ -60,18 +143,15 @@ private:
private:
std::chrono::steady_clock::time_point expiry;
std::vector<u8> midi_notes;
std::vector<Definition> m_definitions;
};
ComboTracker combo;
std::unordered_map<midi::Id, midi::Note> m_id_to_note_mapping;
midi::Note id_to_note(midi::Id id);
rb3drums::KitState parse_midi_message(u8* msg, usz size);
rb3drums::KitState parse_midi_note(u8 id, u8 velocity);
bool is_midi_cc(u8 id, u8 value);
void write_state(u8* buf, const rb3drums::KitState&);
public:
usb_device_rb3_midi_drums(const std::array<u8, 7>& location, const std::string& device_name);
~usb_device_rb3_midi_drums();
void control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) override;
void interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) override;
};

View file

@ -33,7 +33,7 @@ bool cfg_rb3drums::load()
return false;
}
void cfg_rb3drums::save() const
void cfg_rb3drums::save()
{
cfg_log.notice("Saving rb3drums config to '%s'", path);
@ -41,4 +41,6 @@ void cfg_rb3drums::save() const
{
cfg_log.error("Failed to save rb3drums config to '%s' (error=%s)", path, fs::g_tls_error);
}
reload_requested = true;
}

View file

@ -6,22 +6,24 @@ struct cfg_rb3drums final : cfg::node
{
cfg_rb3drums();
bool load();
void save() const;
void save();
cfg::uint<1, 100> pulse_ms{this, "Pulse width ms", 30, true};
cfg::uint<1, 127> minimum_velocity{this, "Minimum velocity", 10, true};
cfg::uint<1, 5000> combo_window_ms{this, "Combo window in milliseconds", 2000, true};
cfg::_bool stagger_cymbals{this, "Stagger cymbal hits", true, true};
cfg::string midi_overrides{this, "Midi id to note override", ""};
cfg::string combo_start{this, "Combo Start", "HihatPedal,HihatPedal,HihatPedal,Snare"};
cfg::string combo_select{this, "Combo Select", "HihatPedal,HihatPedal,HihatPedal,SnareRim"};
cfg::string combo_toggle_hold_kick{this, "Combo Toggle Hold Kick", "HihatPedal,HihatPedal,HihatPedal,Kick"};
cfg::string midi_overrides{this, "Midi id to note override", "", true};
cfg::string combo_start{this, "Combo Start", "HihatPedal,HihatPedal,HihatPedal,Snare", true};
cfg::string combo_select{this, "Combo Select", "HihatPedal,HihatPedal,HihatPedal,SnareRim", true};
cfg::string combo_toggle_hold_kick{this, "Combo Toggle Hold Kick", "HihatPedal,HihatPedal,HihatPedal,Kick", true};
cfg::uint<0, 255> midi_cc_status{this, "Midi CC status", 0xB0, true};
cfg::uint<0, 127> midi_cc_number{this, "Midi CC control number", 4, true};
cfg::uint<0, 127> midi_cc_threshold{this, "Midi CC threshold", 64, true};
cfg::_bool midi_cc_invert_threshold{this, "Midi CC invert threshold", false, true};
const std::string path;
atomic_t<bool> reload_requested = false;
};
extern cfg_rb3drums g_cfg_rb3drums;