Here's the approach I used to get this working over wifi.
Several times per second, the server sends out a separate status packet saying which sample/position it's currently playing. Each client, upon receipt of these packets, compares the server's position against its own.
If server position is less than the client position, then the client slows down playback. Just a bit though, a mere fraction of a percent; enough to accommodate any real clock mismatches plus a little bit more. This is because packet delivery time over wifi is not guaranteed. If latency increases, the clients will receive a server position that's lower than it should be because the packet was delayed, and we don't want the clients to respond strongly every time that happens.
However, if the server position is greater than the client position, it's a different story. We know this is due to either a clock mismatch, or a DROP in latency. In either case, the client can safely speed up playback to match the server position; right up to the limit of being noticeable to the user as a change in playback rate, or less if the mismatch isn't too severe.
This successfully handles short-term variations in latency. They can fluctuate rapidly all over the place, even with packets sporadically being delayed over 250ms, and the system keeps on working perfectly. But that wasn't quite sufficient. From Wikipedia:
"The Haas Effect (or Precedence Effect) is a principle first set forth in 1949 by Helmut Haas, which established that we humans localize sounds by identifying the difference in arrival time between our two ears. The same sound arriving within 25 ms to 35 ms of itself will be suppressed and not be heard as an echo."
If we're playing the same music in several rooms while walking around the house, we don't want it to sound like we're in an echo chamber! And despite what Wikipedia says, I personally find it necessary to keep the music synchronized within 12ms or less to avoid this effect. To achieve that, I also had to account for long-term latency fluctuations; and even the baseline, best-case minimum latency.
Each time a client receives a status packet, it sends a response back to the server. The server measures the time between sending the status packet and getting a response, which it divides by half. This provides an estimate of one-way latency, and it is included in the next status packet sent out to the clients.
The clients maintain their own latency estimate, which is derived from the server's. If the server reports a higher estimate than the client currently has, the client increases its estimate slowly. If the server reports a lower estimate, the client decreases its estimate quickly.
And finally, each time the client receives a status packet from the server, the client adds its latency estimate to the server's position, to get an estimate of the server's true position. That, in turn, is used for the adjustments in playback rate as described above.
I'm not sure if it's quite up to the technical standard of some other systems, which I initially tried mimicking based on whatever info I could scrounge up, and failed at. But the method I came up with works well regardless. Despite some spotty wifi, interference from neighbors, and transmission paths that are regularly walked through, I have not been able to detect any sync issues or echo in almost a year of frequent use.
This is all done on PCs, with a standard media player like MPlayer or VLC handling the actual playback. My software simply controls the player, including reading the player's current position, exchanging synchronization info between server/clients, and adjusting playback rates.
On a resource-limited MCU, you can drop or duplicate individual audio samples, but do this too often and it perceptibly alters the pitch of the audio; so you're limited in how fast you can correct a sync error. If you have more CPU power, you can do it the way MPlayer or VLC do it, with decimation. You can Google the term, but a quick description: This means picking out a segment of audio, that begins and ends at a zero-crossing of amplitude. The segment is then dropped or duplicated, and a blend performed at its edges. It has the advantage of being able to achieve greater changes in speed, without any change of pitch.