If quality isn't an issue, then I would do something similar as TMRpcm does:
- Read the header to figure out the sample rate it should be played (4 bytes at offset 0x18 or 24 as an unsigned long). I don't recommend you exceeding 48 KHz due to RAM and timing constrains.
- Make a "double buffer" of at least 128 bytes each (254 maximum to avoid greater overhead caused by larger-than-8-bits integer arithmetic).
- Create a timed interrupt that fires every period of the file's sample rate (obtained in the first point). This interrupt will update the output to the next sample coming from one of the two buffers and signal a buffer reload when necessary. You can use the TimerOne library for that (period is set in microseconds).
- Handle buffer reloading in the loop() if the Arduino will be dedicated to play a single file without pausing or skipping (or literally anything else). Otherwise it would have to be done in another timed interrupt (and allowing nested interrupts in order to not disrupt the playback itself).
- Fill up both buffers before commencing playback. In a WAV file, samples begin 4 bytes after the string "data" on the header; usually at offset 0x2C or 44 if no ID3 tags or any other metadata was added.
I know this sounds like a lot of work to do, but I couldn't just say reuse the TMRpcm library because it requires an effort on modifying it anyways.
PD: remember to arrange the ladder to the pins 0 to 7 (being the last one the MSB), a direct "port write" will set the state of all these 8 pins at once, also avoiding signal "glitches" in the way.
Also remember that the output has a quite high impedance; so to drive a speaker you need some sort of amplification.