I took a shot at optimizing a bit. the main patch looks good but in addosc there are a few little things (which may add up bc you have 224 of them):
[tabread~] is taking a control-rate input. So, it isn't doing anything that [tabread] couldn't do, and have it translated to a signal later down the graph instead. The difference is that [tabread~] will still process the last input every sample whereas [tabread] will only output when it gets a new message. (so you'll be doing about samplerate times fewer computations if you have a new input every second)
similarly [+~ ] and [*~ ] are more efficient if you use the versions with a control-rate right inlet bc the loops can be vectorized and maybe predicted better. (to do this supply them with a float argument).
generally the arithmetic objects are more efficient than the [expr~] family. And, [expr~ 1 - $v1] is also another case of not needing the signal version bc the input is control-rate (after changing it to [tabread]). Instead you can change it to [1 $1( going into a [- ]. (tho in this case idk if it is actually better than [expr 1 - $f1] bc we also have to process/send the list from the message box.. the main thing is making it control-rate tho)
The final little thing is replacing the division [/ 127] in the adsr subpatch by multiplication with the reciprocal, bc processors are better at multiplying than dividing. You can do this any time you're dividing by a value that won't change.
hope it helps
addosc.pd
as for locality I think the only way is to use $0 in your arrays like you say.. you might consider making addvoice an abstraction as well. Then you could supply $0 as the argument, and inside supply it as the 2nd argument of [addosc] with $1, which could then use it as $2 in the [tabread]s to refer to the grandparent's $0
that way you'd only have to edit 28 [addosc]s instead of 224