Or you could just use Table for all your hashing needs.
Anyway, is using timers really that much of an issue? In all cases, the timer will either match the sound perfectly or take longer than the sound, it will never finish faster than the sound (the ratio is 1:1 at fastest game speed). As such, the sound may not get recycled immediately when it finishes, but will recycle eventually. As such, it is a non-issue unless it's a looping sound, but why would you call ReleaseWhenDone on a non-looping sound?
Also, local player sounds should be a non-issue. You play a sound once, either for all players or a specific player. If you need to play it for another player later, you get a new sound for that; so, the sound will get recycled for all players the moment it finishes for whoever it was played (or later, as mentioned in the above paragraph).
I think the interface for this system could be simplified: a few functions could be merged, since I don't think anyone would ever use them separately anyway (Why would you ask for a sound if you weren't immediately planning on using it?): NewSound, RunSound and RecycleSoundWhenDone. Something like function RunSound takes soundhelper refSound, force playFor returns sound with completely automated recycling.
By the way, what's the point of starting a sound in a new thread?