Hey, this is fun!
My needs are slightly different in that I usually can't tell when a voice is going to become free to be allocated again, so I make it the voice's responsibility to tell me when it's available. Voices also have to figure out when to retrigger, which happens during voice stealing but also when commands are routed back to the voice that's currently handling a previous instance. Here's pseudo code for my allocator:
next command is issued
has it already been assigned a voice?
yes: send next command to that same voice
no: are there free voices?
yes: record that that command is being handled by that voice
note the order that that voice was allocated
send next command to that voice
no: find the oldest allocated voice
record that that command is being handled by that voice
note the order that that voice was (re)allocated
send next command to that voice
when a voice becomes free
add it back into the list of available voices
clear whatever command it was previously associated with
clear whatever order that it was last allocated
And then inside each voice:
next command comes in
what state are we in?
stopped: trigger the action, go to running state
running: retrigger the action
when the action is finished, go to stopped state and tell the allocator you are available