Sunday, 23 January 2022

Analog I/O Example For The NXP LPC55S69-EVK

 I recently wrote some DSP code for the excellent NXP LPC55S69-EVK.

The standard audio I/O example for the LPC55S69-EVK implements a very simple piece of code to read in an array of audio data from the stereo codec and then write it back. It does not show how to access that data or process it using background DSP functions.

I have written an example that uses interrupts, ping-pong buffers and background tasks to apply DSP functions to the real-time audio datastream.

The example can be downloaded from: https://github.com/Numerix-DSP/embedded_projects

Copyright © 2022 Delta Numerix


Version 10.00 Of The SigLib DSP Library Released And Is Now Fully Open Source

SigLib V10 now includes enhanced functions for training and inferring Artificial Intelligence and Machine Learning Convolutional Neural Networks (CNNs). In addition to the traditional DSP functions, the SigLib ML functions are designed for embedded applications such as vibration monitoring etc. They are architected for Edge-AI applications and have been written for the highest level of MIPS and memory optimization.

Containing over 1000 DSP and ML functions, SigLib is now available with a dual open source (GPL) and commercial license and is available from GitHub at: https://github.com/Numerix-DSP/siglib.

Copyright © 2022 Delta Numerix


Friday, 5 November 2021

C/C++ Header Only Library For Reading And Writing .wav Files

I've recently been processing a bunch of .wav files in both C and C++, in order to simplify the transition betweeen the two environments I extracted the .wav files from the Numerix Host Library into a header only library that is now available here: https://github.com/Numerix-DSP/wav_file

It has a very simple API so I hope you find it useful.

Copyright © 2022 Delta Numerix


Tuesday, 19 October 2021

Installing Clang on WSL Ubuntu - Solved

 I recently had to install clang on WSL Ubuntu and found a few conflicts.
The following commands solved the problem:

sudo apt install libc6=2.31-0ubuntu9.2 libc-bin=2.31-0ubuntu9.2
sudo aptitude install clang-12 -f -y
sudo aptitude install clang -y

I then installed clang tidy using:
sudo aptitude install clang-tidy -y

Copyright © 2022 Delta Numerix

Wednesday, 13 October 2021

Processing .wav File Frames Through A Tensorflow Convolutional Neural Network

 I was recently developing a Machine Learning application that would predict classes of audio stored in .wav file.

The .wav file frames are easily processed using numpy to include functions such as the Fast Fourier Transform (FFT).

When it came to processing the frames in the neural network I was stumped by how to translate the frames into a Tensorflow dataset and despite my best efforts I kept getting the following error:

ValueError: Input 0 of layer sequential is incompatible with the layer: : expected min_ndim=3, found ndim=2. Full shape received:

Through much reading of blog posts and finally by trial and error I found the solution so here it is.

I've created this bare minimum example code so that it is easy to follow but it is equally easy to expand the input frame sizes from 3 points, in this example, to as many as you like because the tricky bit is the reshaping and dimension expansion to convert the numpy arrays into the required format for Tensorflow.

Here is the python code, which includes plenty of print statements so that you can visualize how the numpy arrays are modified:

import numpy as np
import tensorflow as tf
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers.convolutional import Conv1D
from keras.layers.convolutional import MaxPooling1D
 
# Top level parameters
n_steps = 3
n_features = 1

X = np.array(         # Source array containing frames of data
  [[123], [234],
   [345], [456],
   [567]])
y = np.array([45678])

print(f'X.shape:\n{X.shape}')
print(f'y.shape:\n{y.shape}')
print(f'X:\n{X}')
print(f'y:\n{y}')

# Reshape and expand dimensions
X = X.reshape((X.shape[0], X.shape[1], n_features))
X = np.expand_dims(np.array(X), n_features)
y = np.expand_dims(np.array(y), n_features)

print(f'X.shape:\n{X.shape}')
print(f'X:\n{X}')

# Define model
model = Sequential()
model.add(Conv1D(filters=64, kernel_size=2, activation='relu', input_shape=(n_steps, n_features)))
model.add(MaxPooling1D(pool_size=2))
model.add(Flatten())
model.add(Dense(1))
model.compile(optimizer='adam', loss='mse')

# Fit model
train_dataset = tf.data.Dataset.from_tensor_slices((X, y))
model.fit(train_dataset, epochs=1000, verbose=0)

# Predict value
x_input = np.array([345])
x_input = x_input.reshape((1, n_steps, n_features))
y_pred = model.predict(x_input, verbose=0)
print(f'y_pred: {y_pred}')

Here are the results:

X.shape:
(5, 3)
y.shape:
(5,)
X:
[[1 2 3]
[2 3 4]
[3 4 5]
[4 5 6]
[5 6 7]]
y:
[4 5 6 7 8]
X.shape:
(5, 1, 3, 1)
X:
[[[[1]
[2]
[3]]] [[[2]
[3]
[4]]] [[[3]
[4]
[5]]] [[[4]
[5]
[6]]] [[[5]
[6]
[7]]]]
y_pred: [[6.0000005]]

We can see that even with the small amount of training data, that the predicted result is suprisingly accurate.

Copyright © 2021 Delta Numerix


Thursday, 24 June 2021

Profiling C/C++ Code Using GCC And Gprof

I recently had to profile a large C code project, to identify functions to target for further optimization and I used gprof for this task.

In this blog post I'm going to take the dot-product code from one of my earlier posts and use that to demonstrate how to profile an application. The original code is available here: https://blog.numerix-dsp.com/2013/01/lets-do-some-digital-signal-processing.html.

Gprof is part of the GNU Binutils package (https://www.gnu.org/software/binutils/) and in general this is installed alongside the standard GCC (build-essential) package with:

    sudo apt install build-essential

In order to generate the profile in a human friendly format (a .png image) we're going to use dot, which is part of the graphviz package. Graphviz can be installed using:

sudo apt install graphviz

We're also going to use gprof2dot (https://pypi.org/project/gprof2dot/). Depending on your Python installation you can use either of the following commands:

    pip:

        pip install gprof2dot

    conda:

        conda install -c conda-forge gprof2dot

Profiling is enabled at compile time using the '-pg' command-line option and inserts instrumentation code into the application to create a statistical approximation of the code execution by, typically, sampling the code execution every 10 milliseconds. This means that to get a good approximation of the function load then the code needs to run for a number of seconds to allow the statistics to be generated accurately. 

Here is the example code, in which main() calls two functions called dotp1() and dotp2(), which in turn call the main dotp() function multiple times to allow the profiler to do its work.

#include <stdio.h>
#define COUNT  48           // The filter size must be modulo 8
#define USE_ARRAY_INDEX 1   // Set to '1' to use array indexing or '0' to use pointers
float a[] = {
 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0,
 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0,
 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, 30.0,
 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0, 40.0,
 41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0
 };
float b[] = {
 41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0,
 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0, 58.0, 59.0, 60.0,
 61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0,
 71.0, 72.0, 73.0, 74.0, 75.0, 76.0, 77.0, 78.0, 79.0, 80.0,
 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0
 };
float dotp (const float *, const float *, const int count);
float dotp1 (const float *, const float *, const int count);
float dotp2 (const float *, const float *, const int count);

void main (void)
{
 float sum;
 sum = dotp1 (a, b, COUNT);
 sum = dotp2 (a, b, COUNT);
 printf ("Dot product = %f\n", sum);
}

float dotp (const float *x, const float *y, const int count)
{
 float sum = 0.0;
 for (int i = 0; i < count; i++) {
#if USE_ARRAY_INDEX
  sum += x[i] * y[i];
#else
  sum += *x++ * *y++;
#endif
 }
 return sum;
}
float dotp1 (const float *x, const float *y, const int count)
{
 float sum = 0.0;
 for (int i = 0; i < 10000000; i++) {
   sum = dotp (x, y, count);
 }
 return sum;
}
float dotp2 (const float *x, const float *y, const int count)
{
 float sum = 0.0;
 for (int i = 0; i < 12000000; i++) {
   sum = dotp (x, y, count);
 }
 return sum;
}

Here is the shell script to build and execute the app then generate the png output file:

#!/bin/bash
# Shell script for compiling app with GCC then profiling the executable

# Compile the app with profiling enabled
gcc dotp_gprof.c -Wall -Wno-main -std=c99 -pg -o dotp_gprof

# Run the app
./dotp_gprof

# Move gmon.out
mv gmon.out dotp_gprof.out

# Extract profile data and write to .dot file
gprof dotp_gprof dotp_gprof.out -Qdotp100 -Qdotp120 | gprof2dot > dotp_gprof.dot

# Convert from dot to png file format
dot -Tpng -odotp_gprof.png dotp_gprof.dot

And here is the .png output:


In the image we can see the following information:

  1. The call graph structure
  2. The number of times each function is called
  3. The percentage of total executation time each function uses, overall and at each level

In this simple application we observe that the majority of the time is spent in the dotp() function, which is to be expected. We can also see the relative time split betweeen the dotp1() and dotp2() functions, given the number of iterations in their 'for' loops.

Copyright © 2021 Delta Numerix