@@ -24,7 +24,7 @@ namespace elem
2424 // Maps incoming MidiEvents to AssignedMidiEvents with polyphonic
2525 // voice assignment.
2626 //
27- // This uses MPE style voice allocation; where assigned voice is designated
27+ // This uses MPE style voice allocation, where assigned voice is designated
2828 // by channel number. That means that we clobber the incoming channel number
2929 // assigned to the original event and rewrite it with a new number corresponding
3030 // to the assigned voice.
@@ -55,42 +55,71 @@ namespace elem
5555 auto outEvent = MidiEvent ((bytes[0 ] & 0xf0 ) | static_cast <uint8_t >(voiceIndex), bytes[1 ], bytes[2 ]);
5656
5757 ctx.outputEvents .addEvent (time, std::move (outEvent));
58- voiceMap[voiceIndex] = bytes[1 ];
58+ voiceMap[voiceIndex].note = static_cast <int32_t >(bytes[1 ]);
59+ voiceMap[voiceIndex].lastModified = steadyClock + time;
5960 }
6061
6162 if (event.message .isNoteOff ()) {
62- auto assignedVoice = std::find (voiceMap.begin (), voiceMap.end (), bytes[1 ]);
63+ auto assignedVoice = std::find_if (voiceMap.begin (), voiceMap.end (),
64+ [&bytes](const Assignment& a) { return a.note == static_cast <int32_t >(bytes[1 ]); });
6365
6466 if (assignedVoice != voiceMap.end ()) {
6567 auto voiceIndex = std::distance (voiceMap.begin (), assignedVoice);
6668 auto outEvent = MidiEvent ((bytes[0 ] & 0xf0 ) | static_cast <uint8_t >(voiceIndex), bytes[1 ], bytes[2 ]);
6769
6870 ctx.outputEvents .addEvent (time, std::move (outEvent));
71+
6972 // Clear the mapping
70- //
71- // TODO: Technically 0 is a valid midi note; maybe just use an int
72- // and let -1 be "unallocated"?
73- voiceMap[voiceIndex] = 0 ;
73+ voiceMap[voiceIndex].note = -1 ;
74+ voiceMap[voiceIndex].lastModified = steadyClock + time;
7475 }
7576 }
7677 });
78+
79+ steadyClock += ctx.numSamples ;
7780 }
7881
7982 size_t getFreeVoice () {
80- auto out = nextFreeVoice;
83+ // The first free voice is the one in voiceMap that has note == -1
84+ // and whose lastModified timestamp is the oldest (i.e. smallest value).
85+ auto nv = numVoices.load ();
86+ size_t bestIndex = 0 ;
87+ int64_t oldestTime = std::numeric_limits<int64_t >::max ();
88+
89+ for (size_t i = 0 ; i < nv; ++i) {
90+ if (voiceMap[i].note == -1 && voiceMap[i].lastModified < oldestTime) {
91+ bestIndex = i;
92+ oldestTime = voiceMap[i].lastModified ;
93+ }
94+ }
8195
82- // TODO: This is round robin, need better
83- if (++nextFreeVoice >= (numVoices.load () - 1 ))
84- nextFreeVoice = 0 ;
96+ // If we found a free voice, return it
97+ if (voiceMap[bestIndex].note == -1 ) {
98+ return bestIndex;
99+ }
85100
86- return out;
101+ // If there is no free voice, we want whichever voiceMap entry has the
102+ // oldest lastModified timestamp, regardless of note value.
103+ for (size_t i = 0 ; i < nv; ++i) {
104+ if (voiceMap[i].lastModified < oldestTime) {
105+ bestIndex = i;
106+ oldestTime = voiceMap[i].lastModified ;
107+ }
108+ }
109+
110+ return bestIndex;
87111 }
88112
89113 // Maps the ith voice to the ith position in the array, where we capture
90114 // the note value the voice was last assigned
91- std::array<uint8_t , 16 > voiceMap;
115+ struct Assignment {
116+ int32_t note = -1 ;
117+ int64_t lastModified = 0 ;
118+ };
119+
120+ std::array<Assignment, 16 > voiceMap;
92121 std::atomic<size_t > numVoices = 1 ;
93- size_t nextFreeVoice = 0 ;
122+ int64_t steadyClock = 0 ;
94123 };
95124
96125 template <typename FloatType>
0 commit comments