I'm developing an audio file spectrum analyzer for a University Project. My main goal is to have an application that plots the Db Spectrum of a 16 Bit WAV PCM audio file (at this time only mono files) in real time and my developing environment is Fedora Workstation 29.
The computational work is done in C++ using the following libraries:
- libsndfile for opening and reading the wav file
- libportaudio for audio playback
- FFTW3 for performing the FFT of each chunk of audio stream
- libboost for IPC
The result of the FFT is then exposed to a Python script through an array of the following struct using a shared memory object (libboost' shared memory object and mapped region).
typedef struct FFTW_Point {
int x_val;
float y_val;
} FFTW_Point;
Plotting is done with Python and PyQtGraph, grabbing the data to be plotted from shared memory.
Everything is fine when plotting just "pure" signals like sine or triangle waves sweeping over time, simple guitar tones or percussion samples.
The problem is that when I try to plot a "real" song i get a strange behavior in the 15 kHz - 20 kHz range, something like an energy cut and it looks really weird
The following code snippets show how I do the critical parts of the task:
Portaudio Callback:
static int pa_callback(
const void *input,
void *output,
unsigned long frameCount,
const PaStreamCallbackTimeInfo *timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData
) {
float *out;
snd_data *p_data = (snd_data*)userData;
sf_count_t num_read;
out = (float*)output;
p_data = (snd_data*)userData;
memset(out, 0, sizeof(float) * frameCount * p_data->info.channels);
num_read = sf_read_float(p_data->file, out, frameCount * p_data->info.channels);
if ((long unsigned int) num_read < frameCount)
{
fftw->setNumSamples(num_read);
fftw->loadInputData(out);
//fftw->writeWav();
return paComplete;
}
fftw->loadInputData(out);
return paContinue;
}
FFTW of audio chunks
inline static double hannWindow(float value, int i, unsigned int size) {
return value * (0.5 * (1 - cos(2*M_PI*i/(size))));
}
void FFTW_Tool::loadInputData(float* buffer) {
for (size_t i = 0; i < numSamples; ++i) {
in[i][0] = hannWindow(buffer[i], i, numSamples);
in[i][1] = 0;
}
StartFFTW();
}
void FFTW_Tool::executePlan(const unsigned int numSamples,
const int direction,
const unsigned int calculationType) {
fftw_plan plan = fftw_plan_dft_1d(
numSamples,
in,
out,
direction,
calculationType
);
fftw_execute(plan);
//shm_region.flush();
//outputSize = numSamples/2
for (size_t i = 0; i < outputSize; ++i) {
sharedStruct[i].x_val = i * samplingFreq / numSamples;
auto real = out[i][0];
auto img = out[i][1];
auto mag = complexModule(real, img);
auto amp = (2*mag)/numSamples;
sharedStruct[i].y_val = amp;
}
fftw_destroy_plan(plan);
std::memcpy(shm_region.get_address(), sharedStruct, (sizeof(FFTW_Point) * outputSize));
}
PyQtGraph update function:
def update(self):
shmem = mmap.mmap(fd, 0, access=mmap.ACCESS_READ)
xs = []
ys = []
for i1, i2, i3, i4, f1, f2, f3, f4 in zip(*[iter(shmem)]*8):
x_val = struct.unpack('i', i1+i2+i3+i4)[0]
y_val = struct.unpack('f', f1+f2+f3+f4)[0]
xs.append(x_val)
ys.append(20*np.log10(y_val))
#ys.append(y_val)
self.set_plotdata(name='spectrum', data_x=xs, data_y=ys)
I really don't know why I get this strange behavior, maybe it is about wrong windowing or maybe it is about the shared memory mechanism and floating point values interpretation.
What bugs me the most is that it should always display wrong data in that frequency range but this happens only with complex audio signals.