electrodyssey.net

Odyssey of Electronics and Computers

DDS sound generator with PortAudio

February 08, 2021 — Nazim

I've been looking for an easy to use and portable library to generate audio tones, so PortAudio seemed like an obvious choice. PortAudio is not only surprisingly easy to use and comes with many excellent examples, but also provides a control over the callback execution latency. You could specify how many samples to be processed with a callback, or let PortAudio decide the optimal count. Although, PortAudio is not officially called a DSP library, it's pretty much useful for DSP. Best of all, you could combine multiple audio samples by adding them together, to get a multi-tone output. Building the library on Windows with VisualStudio was quite easy and is well documented here. All you need to do is disable the console output and unneeded backends (ASIO in my case). To disable the ASIO, navitage to the project explorer and remove the Source Files->hostapi->ASIO. Then, edit the portaudio.def and comment out the exports starting with PaAsio_. Debug console output can be removed from the preprocessor definitions in the project's properties. There been some old trouble reports of Windows 10 not playing well with x86 32-bit compiled PortAudio DLL, so I've built an x64 library which worked great.

Initialization of the audio stream is well described in examples, the only thing which was not that obvious is how-to use a WASAPI output in exclusive mode, so here is the snippet:


outputParameters.device = audio_device_id; /*or use  Pa_GetDefaultOutputDevice()*/
if (outputParameters.device == paNoDevice)
{
    BOOST_LOG(_lg) << "Error: No default output device";
    goto error;
}

outputParameters.channelCount = 2;       /* stereo output */
outputParameters.sampleFormat = paFloat32; /* 32 bit floating point output */
outputParameters.suggestedLatency = Pa_GetDeviceInfo(outputParameters.device)->defaultLowOutputLatency;

struct PaWasapiStreamInfo wasapiInfo;

wasapiInfo.size = sizeof(PaWasapiStreamInfo);
wasapiInfo.hostApiType = paWASAPI;
wasapiInfo.version = 1;
wasapiInfo.flags = (paWinWasapiExclusive | paWinWasapiThreadPriority);
wasapiInfo.threadPriority = eThreadPriorityProAudio;

//default 'shared' mode
if (0 >= exclusive_mode)
    outputParameters.hostApiSpecificStreamInfo = NULL;
else
   //allow exclusive mode
   outputParameters.hostApiSpecificStreamInfo = (&wasapiInfo);

Next we need to create a wave table for looking up the signal phase. Some people prefer to use sin() instead of lookup table, but calculating the sin() each time could introduce a significant load to the system, especially if such callback is executed on the interrupt basis. CORDIC is a nice way to reduce the computational load, but here I'm using the table lookup to keep things simple. The minimal frequency resultion step would be described with the following formula:

f = (tw / 2^23) * 44100

where:

*f* is a resulting frequency

*tw* is a tuning word

44100 is a sampling rate

2^23 is a size of the phase accumulator

This gives us a theoretical minimal frequency step of 0.0052 Hz, which is an overkill, especially if we take into consideration jitter introduced by OS scheduler delays , sound driver layer, e.t.c.

2^23 is roughly an 8meg samples of data. Usually, most hardware DDS generators use shorter wave table with a long phase accumulator register and then truncate the lower bits, but this time I decided to go nuts and make a full size wavetable:(2^23) = 8388608 samples. This was done as an attempt to reduce the spurs associated with phase truncation. I'm going to check whether this helps or not, with a different table sizes later (both truncated and not).

Here is how the tuning word is calculated:

tw = (f / 44100) * 2^23

For example, if our frequency of interest is 1785 Hz , phase shall be incremented by a tuning word of 339538, for each of the subsequent samples. I've checked a sound card output with a scope and it looks like a nice sine, which concludes that our little DDS is functional. Once I'll get a better sound card (and some spare time), I'll measure how different wave table sizes affect the generated signal's FFT.

Tags: portaudio, dsp, sound