Skip to content

Commit

Permalink
MPE Support (#1427)
Browse files Browse the repository at this point in the history
1. MPE, Omni, part channels all working fine. Closes #240
2. MPE mode routes pitch, timbre, channel pressure and bend
3. MPE settable bend ranges
4. Release velocity as a modulator

Closes #437
  • Loading branch information
baconpaul authored Oct 18, 2024
1 parent b840255 commit 5972b44
Show file tree
Hide file tree
Showing 13 changed files with 170 additions and 38 deletions.
27 changes: 26 additions & 1 deletion src-ui/app/shared/PartSidebarCard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ PartSidebarCard::PartSidebarCard(int p, SCXTEditor *e) : part(p), HasEditor(e)
addAndMakeVisible(*outBus);

polyCount = std::make_unique<jcmp::TextPushButton>();
polyCount->setLabel("256");
polyCount->setLabel("512");
polyCount->setOnCallback(editor->makeComingSoon("Polyphony Limit/Monophony"));
addAndMakeVisible(*polyCount);

Expand Down Expand Up @@ -225,10 +225,31 @@ void PartSidebarCard::showMidiModeMenu()
p.addSeparator();
p.addItem("OMNI", true, ch == engine::Part::PartConfiguration::omniChannel,
makeMenuCallback(engine::Part::PartConfiguration::omniChannel));
p.addItem("MPE", true, ch == engine::Part::PartConfiguration::mpeChannel,
makeMenuCallback(engine::Part::PartConfiguration::mpeChannel));
p.addSeparator();
for (int i = 0; i < 16; ++i)
{
p.addItem("Ch. " + std::to_string(i + 1), true, ch == i, makeMenuCallback(i));
}
p.addSeparator();
auto msm = juce::PopupMenu();
msm.addSectionHeader("MPE Settings");
msm.addSeparator();
for (auto d : {12, 24, 48, 96})
{
msm.addItem(std::to_string(d) + " semi bend range", true,
d == editor->partConfigurations[part].mpePitchBendRange,
[d, w = juce::Component::SafePointer(this)] {
if (!w)
return;
w->editor->partConfigurations[w->part].mpePitchBendRange = d;
w->resetFromEditorCache();
w->sendToSerialization(cmsg::UpdatePartFullConfig(
{w->part, w->editor->partConfigurations[w->part]}));
});
}
p.addSubMenu("MPE Settings", msm);
p.showMenuAsync(editor->defaultPopupMenuOptions(midiMode.get()));
}

Expand All @@ -240,6 +261,10 @@ void PartSidebarCard::resetFromEditorCache()
{
midiMode->setLabel("OMNI");
}
else if (mc == engine::Part::PartConfiguration::mpeChannel)
{
midiMode->setLabel("MPE");
}
else
{
midiMode->setLabel(std::to_string(mc + 1));
Expand Down
27 changes: 6 additions & 21 deletions src/engine/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1166,28 +1166,13 @@ void Engine::processNoteOffEvent(int16_t port, int16_t channel, int16_t key, int
voiceManager.processNoteOffEvent(port, channel, key, note_id, velocity);
}

void Engine::MonoVoiceManagerResponder::setMIDIPitchBend(int16_t channel, int16_t pb14bit)
void Engine::onPartConfigurationUpdated()
{
auto fv = (pb14bit - 8192) / 8192.f;
for (const auto &p : engine.getPatch()->getParts())
{
if (p->configuration.active && p->respondsToMIDIChannel(channel))
{
p->pitchBendValue = fv;
}
}
}
void Engine::MonoVoiceManagerResponder::setMIDI1CC(int16_t channel, int16_t cc, int16_t val)
{
auto fv = val / 127.0;
auto midiM = voiceManager_t::MIDI1Dialect::MIDI1;
for (auto &p : *getPatch())
if (p->configuration.channel == Part::PartConfiguration::mpeChannel)
midiM = voiceManager_t::MIDI1Dialect::MIDI1_MPE;

for (const auto &p : engine.getPatch()->getParts())
{
if (p->configuration.active && p->respondsToMIDIChannel(channel))
{
p->midiCCValues[cc] = fv;
}
}
voiceManager.dialect = midiM;
}
void Engine::MonoVoiceManagerResponder::setMIDIChannelPressure(int16_t channel, int16_t pres) {}
} // namespace scxt::engine
6 changes: 5 additions & 1 deletion src/engine/engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ struct Engine : MoveableOnly<Engine>, SampleRateSupport
return idx;
}

void onPartConfigurationUpdated();

tuning::MidikeyRetuner midikeyRetuner;

// new voice manager style
Expand Down Expand Up @@ -213,7 +215,9 @@ struct Engine : MoveableOnly<Engine>, SampleRateSupport
SCLOG("Retrigger Voice Unimplemented")
}

void setVoiceMIDIMPEChannelPitchBend(voice::Voice *v, uint16_t pb14bit) {}
void setVoiceMIDIMPEChannelPitchBend(voice::Voice *v, uint16_t pb14bit);
void setVoiceMIDIMPEChannelPressure(voice::Voice *v, int8_t val);
void setVoiceMIDIMPETimbre(voice::Voice *v, int8_t val);

void setVoicePolyphonicParameterModulation(voice::Voice *v, uint32_t parameter,
double value)
Expand Down
57 changes: 56 additions & 1 deletion src/engine/engine_voice_responder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,11 @@ void Engine::VoiceManagerResponder::endVoiceCreationTransaction(uint16_t port, u
transactionVoiceCount = 0;
}

void Engine::VoiceManagerResponder::releaseVoice(voice::Voice *v, float velocity) { v->release(); }
void Engine::VoiceManagerResponder::releaseVoice(voice::Voice *v, float velocity)
{
v->releaseVelocity = velocity;
v->release();
}

void Engine::VoiceManagerResponder::setNoteExpression(voice::Voice *v, int32_t expression,
double value)
Expand Down Expand Up @@ -213,4 +217,55 @@ void Engine::VoiceManagerResponder::terminateVoice(voice::Voice *v)
v->beginTerminationSequence();
}

void Engine::VoiceManagerResponder::setVoiceMIDIMPEChannelPitchBend(voice::Voice *v,
uint16_t pb14bit)
{
assert(v);
assert(v->zone->parentGroup->parentPart);
assert(v->isVoiceAssigned);
v->mpePitchBend = 1.f * (pb14bit - 8192) / 8192 *
v->zone->parentGroup->parentPart->configuration.mpePitchBendRange;
}

void Engine::VoiceManagerResponder::setVoiceMIDIMPEChannelPressure(voice::Voice *v, int8_t val)
{
assert(v);
assert(v->zone->parentGroup->parentPart);
assert(v->isVoiceAssigned);
v->mpePressure = val / 127.0;
}

void Engine::VoiceManagerResponder::setVoiceMIDIMPETimbre(voice::Voice *v, int8_t val)
{
assert(v);
assert(v->zone->parentGroup->parentPart);
assert(v->isVoiceAssigned);
v->mpeTimbre = val / 127.0;
}

void Engine::MonoVoiceManagerResponder::setMIDIPitchBend(int16_t channel, int16_t pb14bit)
{
auto fv = (pb14bit - 8192) / 8192.f;
for (const auto &p : engine.getPatch()->getParts())
{
if (p->configuration.active && p->respondsToMIDIChannel(channel))
{
p->pitchBendValue = fv;
}
}
}
void Engine::MonoVoiceManagerResponder::setMIDI1CC(int16_t channel, int16_t cc, int16_t val)
{
auto fv = val / 127.0;

for (const auto &p : engine.getPatch()->getParts())
{
if (p->configuration.active && p->respondsToMIDIChannel(channel))
{
p->midiCCValues[cc] = fv;
}
}
}
void Engine::MonoVoiceManagerResponder::setMIDIChannelPressure(int16_t channel, int16_t pres) {}

} // namespace scxt::engine
7 changes: 7 additions & 0 deletions src/engine/group.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,15 @@ struct Group : MoveableOnly<Group>,
float amplitude{1.f}, pan{0.f}, velocitySensitivity{0.6f};
bool muted{false};
bool oversample{true};

ProcRoutingPath procRouting{procRoute_linear};
BusAddress routeTo{DEFAULT_BUS};

bool hasIndependentPolyLimit{false};
bool polyLimitIsVoices{true};
int32_t polyLimit{0};

bool isMonoLegato{false};
} outputInfo;

GroupTriggerConditions triggerConditions;
Expand Down
12 changes: 12 additions & 0 deletions src/engine/part.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,30 @@ struct Part : MoveableOnly<Part>, SampleRateSupport
bool respondsToMIDIChannel(int16_t channel) const
{
return channel < 0 || configuration.channel == PartConfiguration::omniChannel ||
configuration.channel == PartConfiguration::mpeChannel ||
channel == configuration.channel;
}
bool isMPEVoiceChannel(int16_t channel) const
{
return configuration.channel == PartConfiguration::mpeChannel &&
channel != configuration.mpeGlobalChannel;
}

struct PartConfiguration
{
static constexpr int16_t omniChannel{-1};
static constexpr int16_t mpeChannel{-2};

int mpePitchBendRange{24};
int mpeGlobalChannel{0};

bool active{false};
int16_t channel{omniChannel}; // a midi channel or a special value like omni
bool mute{false};
bool solo{false};

bool polyLimitVoices{0}; // poly limit. 0 means unlimited.

BusAddress routeTo{DEFAULT_BUS};
} configuration;
void process(Engine &onto);
Expand Down
35 changes: 25 additions & 10 deletions src/json/engine_traits.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ SC_STREAMDEF(scxt::engine::Engine, SC_FROM({
// Now we need to restore the bus effects
to.getPatch()->setupBussesOnUnstream(to);

to.onPartConfigurationUpdated();

// and finally set the sample rate
to.getPatch()->setSampleRate(to.getSampleRate());
}))
Expand Down Expand Up @@ -149,15 +151,21 @@ SC_STREAMDEF(scxt::engine::Macro, SC_FROM({
findOrSet(v, "nm", scxt::engine::Macro::defaultNameFor(result.index), result.name);
}));

SC_STREAMDEF(
scxt::engine::Part::PartConfiguration,
SC_FROM(v = {{"a", from.active}, {"c", from.channel}, {"m", from.mute}, {"s", from.solo}};),
SC_TO({
findOrSet(v, "c", scxt::engine::Part::PartConfiguration::omniChannel, to.channel);
findOrSet(v, "a", true, to.active);
findOrSet(v, "m", false, to.mute);
findOrSet(v, "s", false, to.solo);
}));
SC_STREAMDEF(scxt::engine::Part::PartConfiguration,
SC_FROM(v = {{"a", from.active},
{"c", from.channel},
{"m", from.mute},
{"s", from.solo},
{"pl", from.polyLimitVoices},
{"mbr", from.mpePitchBendRange}};),
SC_TO({
findOrSet(v, "c", scxt::engine::Part::PartConfiguration::omniChannel, to.channel);
findOrSet(v, "a", true, to.active);
findOrSet(v, "m", false, to.mute);
findOrSet(v, "s", false, to.solo);
findOrSet(v, "pl", 0, to.polyLimitVoices);
findOrSet(v, "mbr", 24, to.mpePitchBendRange);
}));

SC_STREAMDEF(scxt::engine::Part::ZoneMappingItem,
SC_FROM(v = {{"a", from.address},
Expand Down Expand Up @@ -207,6 +215,7 @@ SC_STREAMDEF(
to.parentPatch->parentEngine->getSampleManager()->restoreFromSampleAddressesAndIDs(
samples);

to.parentPatch->parentEngine->onPartConfigurationUpdated();
sg = std::make_unique<engine::Engine::UnstreamGuard>(partStreamingVersion);
}

Expand Down Expand Up @@ -267,7 +276,9 @@ SC_STREAMDEF(scxt::engine::Group::GroupOutputInfo, SC_FROM({
v = {{"amplitude", t.amplitude}, {"pan", t.pan},
{"oversample", t.oversample}, {"velocitySensitivity", t.velocitySensitivity},
{"muted", t.muted}, {"procRouting", t.procRouting},
{"routeTo", (int)t.routeTo}};
{"routeTo", (int)t.routeTo}, {"hip", t.hasIndependentPolyLimit},
{"piv", t.polyLimitIsVoices}, {"pl", t.polyLimit},
{"iml", t.isMonoLegato}};
}),
SC_TO({
findIf(v, "amplitude", result.amplitude);
Expand All @@ -278,6 +289,10 @@ SC_STREAMDEF(scxt::engine::Group::GroupOutputInfo, SC_FROM({
findIf(v, "oversample", result.oversample);
int rt{engine::BusAddress::DEFAULT_BUS};
findIf(v, "routeTo", rt);
findIf(v, "hip", result.hasIndependentPolyLimit);
findIf(v, "piv", result.polyLimitIsVoices);
findIf(v, "pl", result.polyLimit);
findIf(v, "iml", result.isMonoLegato);
result.routeTo = (engine::BusAddress)(rt);
}));

Expand Down
1 change: 1 addition & 0 deletions src/messaging/client/part_messages.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ inline void updatePartFullConfig(const partConfigurationPayload_t &p, const engi
auto [pt, conf] = p;
cont.scheduleAudioThreadCallback([part = pt, configuration = conf](auto &eng) {
eng.getPatch()->getPart(part)->configuration = configuration;
eng.onPartConfigurationUpdated();
});
}
CLIENT_TO_SERIAL(UpdatePartFullConfig, c2s_send_full_part_config, partConfigurationPayload_t,
Expand Down
5 changes: 5 additions & 0 deletions src/modulation/voice_matrix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,14 @@ void MatrixEndpoints::Sources::bind(scxt::voice::modulation::Matrix &m, engine::

m.bindSourceValue(midiSources.modWheelSource, z.parentGroup->parentPart->midiCCValues[1]);
m.bindSourceValue(midiSources.velocitySource, v.velocity);
m.bindSourceValue(midiSources.releaseVelocitySource, v.releaseVelocity);
m.bindSourceValue(midiSources.keytrackSource, v.keytrackPerOct);
m.bindSourceValue(midiSources.polyATSource, v.polyAT);

m.bindSourceValue(mpeSources.mpePressure, v.mpePressure);
m.bindSourceValue(mpeSources.mpeTimbre, v.mpeTimbre);
m.bindSourceValue(mpeSources.mpeBend, v.mpePitchBend);

m.bindSourceValue(noteExpressions.volume, v.noteExpressions[(int)Voice::ExpressionIDs::VOLUME]);
m.bindSourceValue(noteExpressions.pan, v.noteExpressions[(int)Voice::ExpressionIDs::PAN]);
m.bindSourceValue(noteExpressions.tuning, v.noteExpressions[(int)Voice::ExpressionIDs::TUNING]);
Expand Down
21 changes: 18 additions & 3 deletions src/modulation/voice_matrix.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ struct MatrixEndpoints
Sources(engine::Engine *e)
: lfoSources(e), midiCCSources(e), midiSources(e), noteExpressions(e),
aegSource{'zneg', 'aeg ', 0}, eg2Source{'zneg', 'eg2 ', 0}, transportSources(e),
rngSources(e), macroSources(e)
rngSources(e), macroSources(e), mpeSources(e)
{
registerVoiceModSource(e, aegSource, "", "AEG");
registerVoiceModSource(e, eg2Source, "", "EG2");
Expand All @@ -239,18 +239,33 @@ struct MatrixEndpoints
{
MIDISources(engine::Engine *e)
: modWheelSource{'zmid', 'modw'}, velocitySource{'zmid', 'velo'},
keytrackSource{'zmid', 'ktrk'}
releaseVelocitySource{'zmid', 'rvel'}, keytrackSource{'zmid', 'ktrk'}
{
registerVoiceModSource(e, modWheelSource, "MIDI", "Mod Wheel");
MatrixConfig::setDefaultLagFor(modWheelSource, 25);
registerVoiceModSource(e, velocitySource, "MIDI", "Velocity");
registerVoiceModSource(e, releaseVelocitySource, "MIDI", "Release Vel");
registerVoiceModSource(e, keytrackSource, "MIDI", "KeyTrack");
registerVoiceModSource(e, polyATSource, "MIDI", "Poly AT");
MatrixConfig::setDefaultLagFor(polyATSource, 100);
}
SR modWheelSource, velocitySource, keytrackSource, polyATSource;
SR modWheelSource, velocitySource, releaseVelocitySource, keytrackSource, polyATSource;
} midiSources;

struct MPESources
{
MPESources(engine::Engine *e)
: mpeBend{'zmpe', 'bend'}, mpeTimbre{'zmpe', 'timb'}, mpePressure{'zmpe', 'pres'}
{
registerVoiceModSource(e, mpeBend, "MPE", "Voice Pitch Bend");
registerVoiceModSource(e, mpeTimbre, "MPE", "Timbre");
MatrixConfig::setDefaultLagFor(mpeTimbre, 50);
registerVoiceModSource(e, mpePressure, "MPE", "Pressure");
MatrixConfig::setDefaultLagFor(mpePressure, 50);
}
SR mpeBend, mpeTimbre, mpePressure;
} mpeSources;

struct NoteExpressionSources
{
NoteExpressionSources(engine::Engine *e)
Expand Down
1 change: 1 addition & 0 deletions src/voice/voice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,7 @@ float Voice::calculateVoicePitch()

fpitch += retuner;
fpitch += noteExpressions[(int)ExpressionIDs::TUNING];
fpitch += mpePitchBend;

keytrackPerOct = (key + retuner - zone->mapping.rootKey) / 12.0;

Expand Down
7 changes: 7 additions & 0 deletions src/voice/voice.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,14 @@ struct alignas(16) Voice : MoveableOnly<Voice>,
uint8_t originalMidiKey{
60}; // the actual physical key pressed not the one I resolved to after tuning
float velocity{1.f};
float releaseVelocity{0.f};
float velKeyFade{1.f};
float keytrackPerOct{0.f}; // resolvee key - pitch cnter / 12
float polyAT{0.f};
float mpePitchBend{0.f}; // in semis
float mpeTimbre{0.f}; // 0..1 normalized
float mpePressure{0.f}; // 0..1 normalized

static constexpr size_t noteExpressionCount{7};
float noteExpressions[noteExpressionCount]{};
// These are the same as teh CLAP expression IDs but I dont want to include
Expand Down Expand Up @@ -203,6 +208,8 @@ struct alignas(16) Voice : MoveableOnly<Voice>,
isVoicePlaying = true;
isVoiceAssigned = true;

releaseVelocity = 0.f;

voiceStarted();
}
void release() { isGated = false; }
Expand Down

0 comments on commit 5972b44

Please sign in to comment.