Skip to content

Commit

Permalink
Merge pull request #19777 from alexpavlov96/midi_snd
Browse files Browse the repository at this point in the history
fix #19096: brought back snd render for midi-export
  • Loading branch information
alexpavlov96 committed Oct 20, 2023
2 parents c8d643a + 1573ab3 commit 69a8e1e
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 3 deletions.
15 changes: 15 additions & 0 deletions src/engraving/compat/midi/compatmidirender.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ void CompatMidiRender::renderScore(Score* score, EventsHolder& events, const Com
internal.renderScore(events, ctx, expandRepeats);
}

int CompatMidiRender::getControllerForSnd(Score* score, int globalSndController)
{
const int DEFAULT_CC = CTRL_BREATH;

SynthesizerState s = score->synthesizerState();
int method = s.method();
int controller = s.ccToUse();

if (method == -1) {
controller = (globalSndController == -1) ? DEFAULT_CC : globalSndController;
}

return controller;
}

void CompatMidiRender::createPlayEvents(const Score* score, Measure const* start, Measure const* const end)
{
if (!start) {
Expand Down
1 change: 1 addition & 0 deletions src/engraving/compat/midi/compatmidirender.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class CompatMidiRender
{
public:
static void renderScore(Score* score, EventsHolder& events, const CompatMidiRendererInternal::Context& ctx, bool expandRepeats);
static int getControllerForSnd(Score* score, int globalSndController);
static void createPlayEvents(const Score* score, Measure const* start = nullptr, Measure const* end = nullptr);
static void createPlayEvents(const Score* score, Chord* chord, Chord* prevChord = nullptr, Chord* nextChord = nullptr);
private:
Expand Down
66 changes: 66 additions & 0 deletions src/engraving/compat/midi/compatmidirenderinternal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,68 @@ static bool letRingShouldApply(const NoteEvent& event, const Note* note)
return true;
}

static void renderSnd(EventsHolder& events, const Chord* chord, int noteChannel, int tickOffset, int sndController)
{
Staff* staff = chord->staff();
ChangeMap& veloEvents = staff->velocities();
ChangeMap& multEvents = staff->velocityMultiplications();
Fraction stick = chord->tick();
Fraction etick = stick + chord->ticks();
auto changes = veloEvents.changesInRange(stick, etick);
auto multChanges = multEvents.changesInRange(stick, etick);

std::map<int, int> velocityMap;
for (auto& change : changes) {
int lastVal = -1;
int endPoint = change.second.ticks();
for (int t = change.first.ticks(); t <= endPoint; t++) {
int velo = veloEvents.val(Fraction::fromTicks(t));
if (velo == lastVal) {
continue;
}
lastVal = velo;

velocityMap[t] = velo;
}
}

double CONVERSION_FACTOR = CompatMidiRendererInternal::ARTICULATION_CONV_FACTOR;
for (auto& change : multChanges) {
// Ignore fix events: they are available as cached ramp starts
// and considering them ends up with multiplying twice effectively
if (change.first == change.second) {
continue;
}

int lastVal = CompatMidiRendererInternal::ARTICULATION_CONV_FACTOR;
int endPoint = change.second.ticks();
int lastVelocity = velocityMap.upper_bound(change.first.ticks())->second;
for (int t = change.first.ticks(); t <= endPoint; t++) {
int mult = multEvents.val(Fraction::fromTicks(t));
if (mult == lastVal || mult == CONVERSION_FACTOR) {
continue;
}
lastVal = mult;

double realMult = mult / CONVERSION_FACTOR;
if (velocityMap.find(t) != velocityMap.end()) {
lastVelocity = velocityMap[t];
velocityMap[t] *= realMult;
} else {
velocityMap[t] = lastVelocity * realMult;
}
}
}

for (auto point = velocityMap.cbegin(); point != velocityMap.cend(); ++point) {
// NOTE:JT if we ever want to use poly aftertouch instead of CC, this is where we want to
// be using it. Instead of ME_CONTROLLER, use ME_POLYAFTER (but duplicate for each note in chord)
NPlayEvent event = NPlayEvent(ME_CONTROLLER, noteChannel, sndController, std::clamp(point->second, 0, 127));
event.setOriginatingStaff(chord->staffIdx());
events[noteChannel].insert(std::make_pair(point->first + tickOffset, event));
}
}

//---------------------------------------------------------
// collectNote
//---------------------------------------------------------
Expand Down Expand Up @@ -471,6 +533,10 @@ static void collectNote(EventsHolder& events, const Note* note, const CollectNot
}
}

if (instr->singleNoteDynamics()) {
renderSnd(events, chord, noteChannel, noteParams.tickOffset, context.sndController);
}

// Bends
for (EngravingItem* e : note->el()) {
if (!e || (e->type() != ElementType::BEND)) {
Expand Down
2 changes: 1 addition & 1 deletion src/engraving/compat/midi/compatmidirenderinternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ class CompatMidiRendererInternal

struct Context
{
SynthesizerState synthState;
int sndController = CTRL_BREATH;
bool metronome = true;
std::shared_ptr<ChannelLookup> channels = std::make_shared<ChannelLookup>();

Expand Down
162 changes: 162 additions & 0 deletions src/engraving/tests/midirenderer_data/breath_controller.mscx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<museScore version="4.20">
<programVersion>4.2.0</programVersion>
<programRevision></programRevision>
<Score>
<Division>480</Division>
<showInvisible>1</showInvisible>
<showUnprintable>1</showUnprintable>
<showFrames>1</showFrames>
<showMargins>0</showMargins>
<open>1</open>
<metaTag name="arranger"></metaTag>
<metaTag name="composer">Composer / arranger</metaTag>
<metaTag name="copyright"></metaTag>
<metaTag name="creationDate">2023-10-10</metaTag>
<metaTag name="lyricist"></metaTag>
<metaTag name="movementNumber"></metaTag>
<metaTag name="movementTitle"></metaTag>
<metaTag name="platform">Microsoft Windows</metaTag>
<metaTag name="poet"></metaTag>
<metaTag name="source"></metaTag>
<metaTag name="sourceRevisionId"></metaTag>
<metaTag name="subtitle">Subtitle</metaTag>
<metaTag name="translator"></metaTag>
<metaTag name="workNumber"></metaTag>
<metaTag name="workTitle">Untitled score</metaTag>
<Order id="orchestral">
<name>Orchestral</name>
<instrument id="flute">
<family id="flutes">Flutes</family>
</instrument>
<section id="woodwind" brackets="true" barLineSpan="true" thinBrackets="true">
<family>flutes</family>
<family>oboes</family>
<family>clarinets</family>
<family>saxophones</family>
<family>bassoons</family>
<unsorted group="woodwinds"/>
</section>
<section id="brass" brackets="true" barLineSpan="true" thinBrackets="true">
<family>horns</family>
<family>trumpets</family>
<family>cornets</family>
<family>flugelhorns</family>
<family>trombones</family>
<family>tubas</family>
</section>
<section id="timpani" brackets="true" barLineSpan="true" thinBrackets="true">
<family>timpani</family>
</section>
<section id="percussion" brackets="true" barLineSpan="true" thinBrackets="true">
<family>keyboard-percussion</family>
<family>drums</family>
<family>unpitched-metal-percussion</family>
<family>unpitched-wooden-percussion</family>
<family>other-percussion</family>
</section>
<family>keyboards</family>
<family>harps</family>
<family>organs</family>
<family>synths</family>
<soloists/>
<section id="voices" brackets="true" barLineSpan="false" thinBrackets="true">
<family>voices</family>
<family>voice-groups</family>
</section>
<section id="strings" brackets="true" barLineSpan="true" thinBrackets="true">
<family>orchestral-strings</family>
</section>
<unsorted/>
</Order>
<Part id="1">
<Staff id="1">
<StaffType group="pitched">
<name>stdNormal</name>
</StaffType>
</Staff>
<trackName>Flute</trackName>
<Instrument id="flute">
<longName>Flute</longName>
<shortName>Fl.</shortName>
<trackName>Flute</trackName>
<minPitchP>59</minPitchP>
<maxPitchP>98</maxPitchP>
<minPitchA>60</minPitchA>
<maxPitchA>93</maxPitchA>
<instrumentId>wind.flutes.flute</instrumentId>
<Channel>
<program value="73"/>
<synti>Fluid</synti>
</Channel>
</Instrument>
</Part>
<Staff id="1">
<VBox>
<height>10</height>
<excludeFromParts>0</excludeFromParts>
<Text>
<style>title</style>
<text>Untitled score</text>
</Text>
<Text>
<style>subtitle</style>
<text>Subtitle</text>
</Text>
<Text>
<style>composer</style>
<text>Composer / arranger</text>
</Text>
</VBox>
<Measure>
<voice>
<TimeSig>
<sigN>4</sigN>
<sigD>4</sigD>
</TimeSig>
<Dynamic>
<subtype>ppp</subtype>
<velocity>16</velocity>
</Dynamic>
<Spanner type="HairPin">
<HairPin>
<subtype>0</subtype>
</HairPin>
<next>
<location>
<measures>1</measures>
</location>
</next>
</Spanner>
<Chord>
<durationType>whole</durationType>
<Note>
<pitch>72</pitch>
<tpc>14</tpc>
</Note>
</Chord>
</voice>
</Measure>
<Measure>
<voice>
<Dynamic>
<subtype>fff</subtype>
<velocity>126</velocity>
<offset x="-0.384535" y="2.73072"/>
</Dynamic>
<Spanner type="HairPin">
<prev>
<location>
<measures>-1</measures>
</location>
</prev>
</Spanner>
<Rest>
<durationType>measure</durationType>
<duration>4/4</duration>
</Rest>
</voice>
</Measure>
</Staff>
</Score>
</museScore>
33 changes: 32 additions & 1 deletion src/engraving/tests/midirenderer_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ static EventsHolder renderMidiEvents(const String& fileName, bool eachStringHasC
EventsHolder events;
CompatMidiRendererInternal::Context ctx;

ctx.synthState = mu::engraving::SynthesizerState();
ctx.metronome = false;
ctx.eachStringHasChannel = eachStringHasChannel;
ctx.instrumentsHaveEffects = instrumentsHaveEffects;
Expand All @@ -106,6 +105,21 @@ static EventsHolder getNoteOnEvents(const EventsHolder& events)
return filteredEventMap;
}

static EventsHolder getControllerEvents(const EventsHolder& events)
{
EventsHolder filteredEventMap;
for (size_t i = 0; i < events.size(); ++i) {
for (auto ev: events[i]) {
if (ev.second.type() != EventType::ME_CONTROLLER) {
continue;
}
filteredEventMap[i].insert({ ev.first, ev.second });
}
}

return filteredEventMap;
}

/*****************************************************************************
ENABLED TESTS BELOW
Expand Down Expand Up @@ -801,6 +815,23 @@ TEST_F(MidiRenderer_Tests, slideInAfterRest)
checkEventInterval(events, 480, 959, 63, defVol);
}

TEST_F(MidiRenderer_Tests, breathController)
{
EventsHolder events = getControllerEvents(renderMidiEvents(u"breath_controller.mscx"));

EXPECT_EQ(events.size(), 1);
EXPECT_EQ(events[DEFAULT_CHANNEL].size(), 111);

int prevBreathControllerVal = -1;

for (auto& ev : events[DEFAULT_CHANNEL]) {
EXPECT_EQ(ev.second.dataA(), CTRL_BREATH);
int breathControllerVal = ev.second.dataB();
EXPECT_TRUE(breathControllerVal > prevBreathControllerVal);
prevBreathControllerVal = breathControllerVal;
}
}

/*****************************************************************************
DISABLED TESTS BELOW
Expand Down
3 changes: 2 additions & 1 deletion src/importexport/midi/internal/midiexport/exportmidi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ bool ExportMidi::write(QIODevice* device, bool midiExpandRepeats, bool exportRPN
ctx.eachStringHasChannel = false;
ctx.instrumentsHaveEffects = false;
ctx.metronome = false;
ctx.synthState = synthState;
ctx.sndController = CompatMidiRender::getControllerForSnd(m_score, synthState.ccToUse());

CompatMidiRender::renderScore(m_score, events, ctx, midiExpandRepeats);

m_pauseMap.calculate(m_score);
Expand Down

0 comments on commit 69a8e1e

Please sign in to comment.