@@ -106,6 +106,16 @@ class VoicePool {
106106 }
107107 }
108108
109+ size_t CountNumActiveVoices () {
110+ size_t count = 0 ;
111+ for (VoiceIndex idx = 0 ; idx < mVoices .size (); idx++) {
112+ if (mVoices [idx].activeNote ) {
113+ count++;
114+ }
115+ }
116+ return count;
117+ }
118+
109119 private:
110120 using VoiceIndex = size_t ;
111121 void StopVoice (VoiceIndex index) {
@@ -134,51 +144,76 @@ class VoicePool {
134144 struct VoiceData {
135145 explicit VoiceData (TProcessor& p) : voice(p) {}
136146 TVoice voice;
137- std::optional<NoteTuple> activeNote;
147+ std::optional<NoteTuple> activeNote{};
148+ bool isPressed;
149+ int32_t age;
138150 };
139151
140152 TProcessor& mProcessor ;
141153 etl::vector<VoiceData, TMaxVoices> mVoices ;
142154 VoiceStrategy mStrategy {};
143155
144156 struct PolyVoiceStrategy {
145- void Clear () { mVoicesByLastUsed . clear () ; }
157+ void Clear () { mNextAge = 0 ; }
146158 void ProcessNoteOn (VoicePool& pool, const NoteTuple& note, float velocity) {
147- // retrigger existing voice, if there is one
148- VoiceIndex voiceIndex = SIZE_MAX;
159+ VoiceIndex nextVoiceIndex = SIZE_MAX;
160+
161+ auto compare = [&](const VoiceData& left, const VoiceData& right) {
162+ // priority #1: is note not active?
163+ if (left.activeNote == std::nullopt && right.activeNote != std::nullopt ) {
164+ return -1 ;
165+ }
166+ if (right.activeNote == std::nullopt && left.activeNote != std::nullopt ) {
167+ return 1 ;
168+ }
169+ // priority #2: is note not pressed?
170+ if (!left.isPressed && right.isPressed ) {
171+ return -1 ;
172+ }
173+ if (!right.isPressed && left.isPressed ) {
174+ return 1 ;
175+ }
176+ // tiebreaker: least recently used
177+ if (left.age != right.age ) {
178+ return left.age < right.age ? -1 : 1 ;
179+ }
180+ return 0 ;
181+ };
182+
149183 for (VoiceIndex idx = 0 ; idx < pool.mVoices .size (); idx++) {
150- auto & data = pool.mVoices [idx];
151- if (data. activeNote && data. activeNote ->Match (note)) {
152- voiceIndex = idx;
153- pool. SendNoteEnd (*(data. activeNote )) ;
184+ auto & voiceNote = pool.mVoices [idx]. activeNote ;
185+ if (voiceNote && voiceNote ->Match (note)) {
186+ // always win: does existing note match?
187+ nextVoiceIndex = idx ;
154188 break ;
189+ } else if (nextVoiceIndex == SIZE_MAX) {
190+ nextVoiceIndex = idx;
191+ } else if (compare (pool.mVoices [idx], pool.mVoices [nextVoiceIndex]) == -1 ) {
192+ nextVoiceIndex = idx;
155193 }
156- if (data.activeNote == std::nullopt && voiceIndex == SIZE_MAX) {
157- voiceIndex = idx;
158- }
159- }
160- // no voice available, time to steal
161- if (voiceIndex == SIZE_MAX) {
162- voiceIndex = mVoicesByLastUsed .front ();
163- mVoicesByLastUsed .pop (); // from front
164- auto & data = pool.mVoices [voiceIndex];
165- pool.SendNoteEnd (*(data.activeNote ));
166194 }
167195
168- mVoicesByLastUsed .push (voiceIndex); // to back
169- pool.mVoices [voiceIndex].activeNote = note;
170- pool.mVoices [voiceIndex].voice .ProcessNoteOn (note, velocity);
196+ auto & nextVoiceData = pool.mVoices [nextVoiceIndex];
197+ if (nextVoiceData.activeNote ) {
198+ pool.SendNoteEnd (*(nextVoiceData.activeNote ));
199+ }
200+ nextVoiceData.activeNote = note;
201+ nextVoiceData.voice .ProcessNoteOn (note, velocity);
202+ nextVoiceData.isPressed = true ;
203+ nextVoiceData.age = mNextAge ++;
171204 }
172205 void ProcessNoteOff (VoicePool& pool, const NoteTuple& note) {
173206 auto & mVoices = pool.mVoices ;
174207 for (VoiceIndex idx = 0 ; idx < mVoices .size (); idx++) {
175208 auto & data = mVoices [idx];
176209 if (data.activeNote && data.activeNote ->Match (note)) {
177210 data.voice .ProcessNoteOff ();
211+ data.isPressed = false ;
178212 }
179213 }
180214 }
181- etl::circular_buffer<VoiceIndex, TMaxVoices> mVoicesByLastUsed {};
215+
216+ int32_t mNextAge =0 ;
182217 } mPolyVoiceStrategy {};
183218
184219 struct MonoLastVoiceStrategy {
0 commit comments