IOAudioEngine expects the chip driver to maintain a DMA ring buffer from which samples are fed to the playback channel. The driver today uses a 128KB buffer, which is enough for about 680 milliseconds using a 48KHz sample rate. The chip driver is expected to provide the following functions
Thanks for the quick reply. Yes, I'd (and maybe others too) like to hear the long answer.
- report an interrupt every time the buffer wraps around.
- report an accurate position of the DMA transfer pointer whenever IOAudioEngine asks for it.
IOAudioEngine reports three parameters to the user-clients via shared memory
- The number of times the buffer has wrapped around since playback started.
- A timestamp of the last time the buffer wrapped around (based on mach_absolute_time() - this is a nanosecond counter based on the TSC).
- The last position of the erase head.
The ES137x chipset supports both needed features - generating interrupts and reporting a DMA pointer.
And now for the reason this doesn't work.... [are you ready?]
VMware doesn't emulate the ES137x's DMA pointer
I suspect the reason they don't is because they can't. They probably pass the buffer on to the host's audio driver and tell it how many samples to play. They can't tell where the DMA pointer is exacly, but they can generate interrupts in the guest whenever the sample counter completes. So what they do instead is
- When the channel is off, the DMA pointer is always reported as zero.
- When the channel is on, they report the last known value for the pointer when an interrupt fires. This value stays the same until the next interrupt or until the channel is turned off.
The first version (1.0.0) used a 64K buffer, the DMA pointer always stayed at zero. This means the erase head never got run, and the user-clients had no advance notification that any part of the buffer has already been played and can be refilled.
It's possible to increase the number of interrupts per buffer from the host with "pciSound.DAC2InterruptPerSec". For version 1.0.0, this caused the driver to report every interrupt up the stack, which isn't what the user-clients expect. The current version 1.0.1 always reports one interrupt per buffer up the stack, but multiple interrupt per buffer will cause the DMA pointer to have a correct value at a higher frequency per buffer. In theory, higher values of the number of interrupts per buffer should make the driver resemble an accurate DMA pointer more closely, but for some reason I can't figure out this doesn't work. If I increase the number of interrupt per buffer from 2 to 4, 8 or 16 things start getting worse.
There are still some other things that can be tried
- Even at multiple interrupts per buffer, IOAudioEngine always runs its erase head 4 times per buffer. This means there can be a delay of up to a quarter-buffer between the time the interrupt is fired and the time IOAudioEngine performs the erasure. This means that the time window the user-clients have for detecting the erasure after an interrupt can be reduced down from half a buffer to as little as about a quarter of a buffer. One thing that can be done is to try to browbeat IOAudioEngine into running its erase-head "immediately" after an interrupt instead of on a timer. Due to the way AppleAC97Audio is designed, this requires some non-trivial redesigning of the code, or some ugly hacks. Accomplishing this can make sure that user-clients have about a half-buffer of advance notice to fill the buffer.
- IOAudioEngine and IOAudioEngineUserClient have functions for helping the user-clients do their timing performClientOutput and calculateSampleTimeout. I haven't taken a good look at those. There might be a way to help prod the user-client into refilling the buffer sooner.
Ultimately the stuttering is caused by user-clients falling behind when filling the buffer. The duration of an audio sample is about 20 microseconds. Since the CPU/RAM system is about 4 orders of magnitude faster than that, once the user-client gets some CPU time, it quickly makes up the lag. That's why these imperfections sound the way they do - short and random. The goal is to get the user-client not to fall behind at all.
There's another issue - I'm testing the driver on a dual-core with hardware virtualization, and I'm not really hearing any stuttering at all anymore. I suspect people still hearing it are using software virtualization - although I can't be sure. The problem is that in order to test if modifications I make are helping, I need a machine that can reliably reproduce the problem, and I don't have one.