Core Audio: sine wave gets distorted over time

Hello, I am using Core Audio to output a sine wave at a constant frequency (256Hz). The problem I have is that the sound starts very nice and pure, but gets distored over time - feels like there is some sort of a cumulative error which gets worse as the time goes by.

I am using AudioDeviceCreateIOProcID to create a callback, in which I populate the buffer with samples. I only have a single buffer, because my samples are interleaved. Buffer size is always constant (12800 bytes). Samples are floats (from -1 to 1).

Here is what I tried to identify the reasons for distortion:

  • I validated that each subsequent callback starts generating samples with the proper phase i.e. the one at which the previous callback ended. E.g. if the last sample from previous callback was 0.8f, then the first callback in next callback is going to be 0.82f as expected.
  • I was wondering if maybe hardware plays the buffer when I am filling it, so I even used mutex to lock the buffer as I am writing to it, but it did not do anything at all. This probably means that the buffer that is passed to callback by OS is already safe to write to.
  • I inspected AudioStreamBasicDescription, buffer size and how many bytes I write to the buffer - it all matches my expectations.

Any ideas on what might be causing this sound distortion over time?

  • Just in case - here is the source code:

    audio initialization: https://github.com/DataDrivenEngineer/antiqua/blob/main/antiqua/main.m#L96audio callback: https://github.com/DataDrivenEngineer/antiqua/blob/main/antiqua/main.m#L23sine wave generation: https://github.com/DataDrivenEngineer/antiqua/blob/main/antiqua/antiqua.cpp#L26
Add a Comment

Accepted Reply

r32 t = 2.f * PI32 * (r32) soundState->runningSampleCount / framesPerCycle;
r32 sineValue = sinf(t);
….
soundState->runningSampleCount++;

What is r32? Does runningSampleCount ever return to 0, or does it just keep increasing forever?

I suspect that you are relying on sin(largenumber) == sin(largenumber mod 2pi), which is only approximately true.

If runningSampleCount and framesPerCylce are both integers, try this:

t = 2pi * (double)(runningSampleCount % framesPerCycle) / framesPerCycle

Replies

r32 t = 2.f * PI32 * (r32) soundState->runningSampleCount / framesPerCycle;
r32 sineValue = sinf(t);
….
soundState->runningSampleCount++;

What is r32? Does runningSampleCount ever return to 0, or does it just keep increasing forever?

I suspect that you are relying on sin(largenumber) == sin(largenumber mod 2pi), which is only approximately true.

If runningSampleCount and framesPerCylce are both integers, try this:

t = 2pi * (double)(runningSampleCount % framesPerCycle) / framesPerCycle

Thank you so much! You were absolutely correct. r32 is an alias for float. runningSampleIndex is increasing indefinitely, and the larger the number, the more float loses precision.

The solution was to not grow runningSameIndex indefinitely, but reset it every period, so that it always stays in [0, framesPerPeriod] range.