This weekend I spent most of it trying to create a Finite Impulse Response filter in C++ using fmod, a audio library.

The main way to manipulate a carrier signal or some input signal like some sound or music playing and to say remove the high-frequency components is that you need to perform a mathematical processed called convolution, whereby you 'convolve' the input with what is called filter coefficients that adapt and modify the input amplitudes such that they filter out and remove those components. The convolution theorem looks like this:

This specifically was the task:

Program a controllable FIR filter, i.e. a filter that changes its characteristics over time in response to a control signal. To change in the characteristics of the filter, you need to change the filter coefficients. If you do the games track, implement the filter for real time use with a circular buffer in an FMOD custom DSP and implement a trigger in the demo that changes the filter

The thing that I needed to do was implement a circular buffer to store previous ie historical samples so that the 'b' coefficients, which require previous samples to work with can be applied to them.

Digital Signal Processing is quite an interesting subject, there is a great book, called The Scientist and Engineer's Guide to Digital Signal Processing that discusses many aspects of it here and it's quite comprehensive and it's free!

I think what I've enjoyed the most so far is actually the math, seeing the calculations translate into practical applications and I think this is what has helped me overcome some parts that were a bit daunting at first, especially the sinusoidal nature of signals, the Fourier transform and the complex number system.

So I spent most of that day coding up a circular buffer to store those samples so I can buffer audio that can be used for my filter. The theory behind circular buffers can be seen here: https://en.wikipedia.org/wiki/Circular_buffer, it's mostly quite clear and if you're going to be doing any type of real-time processing, its essential.

Once that was done, I was then able to use that buffer to store the previous n samples, apply the convolution to those previous samples and thereby in effect applying the filter to the input signal such that the output signal would hold those properties, which was to allow input frequencies of 0-1000 Hz through and removes those > 1000 Hz.

It sounds all a bit 'convolved' (see what I did there!) just to remove some frequencies from the signal but that's the point I think, to realise how much goes into it, and in doing the practicals you can appreciate the theory and test your understanding. 

I finally settled on a testing framework(Google Test) and used that to test it the implementation of my circular buffer. It's not production-ready by any stretch of the imagination because it doesn't really check bounds or other edge cases or anything silly that you try and do with it, however for the task at hand, it works just great.  So it's more of a prototype and a learning tool which perhaps might be a better way of putting it.

Anyway, this is how it ended up looking:

#pragma once
#include <vector>
using namespace std;

class CircularBufferTests_PreviousN_Test;

/*
A very simple circular buffer for my DSP assignment
*/
template <typename T>
class cbuf
{
	friend CircularBufferTests_PreviousN_Test; // For testing

private:
	int m_Size; // size of the buffer. It is fixed.
	vector<T> m_Buffer; // underlying buffer, buffer[0] is considered the first item
	int m_WriteIndex;
	int m_ReadIndex;

	// Internal functions for calulating indexes relative to n (last sample)
	int GetPrevNIndex(int n = 0) { return Wrap(GetNewestIndex() - n, m_Size); }
	int Wrap(int n, int arrayLength) { return ((n % arrayLength) + arrayLength) % arrayLength; }

public:
	cbuf(int size) :m_Size(size), m_WriteIndex(0), m_ReadIndex(0), m_Buffer(vector<T>(size)) {}
	
	~cbuf() { m_Buffer.clear(); m_Buffer = { 0 }; }

	// Expose the underlying vector for direct poiinter maipultion.
	// https://stackoverflow.com/questions/2923272/how-to-convert-vector-to-array
	T* ToArray() { return  &m_Buffer[0];	}

	// Overwrite oldest entry, returns the index of the item saved
	int Put(T item)
	{
		m_Buffer[GetWriteIndex()] = item;
		m_ReadIndex = GetWriteIndex();
		m_WriteIndex = (m_WriteIndex + 1) % m_Size;
		return m_ReadIndex;
	}

	// Read the value most recently added
	T ReadNewestHead() { return m_Buffer[m_ReadIndex]; }

	// Get the index of the last added item ie the newest
	int GetNewestIndex() { return m_ReadIndex; }

	// Read at a specific index. No bounds checking.
	T ReadAtIndex(int i) { return m_Buffer[i]; }

	// Read at value from the back of the buffer
	T ReadFromBack(int n = 0) { return m_Buffer[(m_Size - 1) - n]; }

	// Use index notation to reference items relative to the most recent item. ie n
	// Supports notation like -3 as in n-3 as well as 0 which means n
	// Not tested when n < 0
	T ReadN(int n = 0) { return m_Buffer[GetPrevNIndex(-n)]; }

	// Get the value that will be overwritten next
	T ReadOldest() { return m_Buffer[m_WriteIndex]; }

	// Get the index of the value that is going to be overwritten by next Put
	int GetOldIndex() { return m_WriteIndex; }

	// same as GetOldIndex()
	int GetWriteIndex() { return GetOldIndex(); }

	// Size of the underlying buffer.
	int GetSize() { return m_Buffer.size(); }

	void PrintContents()
	{
		cout << "[";
		for (int i = 0; i < m_Buffer.size(); i++)
		{
			std::cout << m_Buffer[i];
			if (i !a= m_Buffer.size())
				cout << ",";
		}
		cout << "]" << endl;;
	}
};

What I particularly like about my implementation is that you can refer to the last n values stored in it in the same way the algorithm refers to samples ie n-k, so you can see in my implementation of the convolution algorithm below, that I read the last n samples stored in the buffer using the notation: prevBuff->ReadN(-3) to signify x[n-3] ie the 3rd previous sample in x.

The convolution algorithm I put together to implement it is here and most of this is all on Github anyway: 

#pragma once
class ConvolutionHelper
{
public:
	// Swaps the signal and convolves the b coeficcients using previous values stored in circular buffer, updates the buffer
	static void ConvolveXn(float* chunk, const int numSamplesPerChunk, int n, float* yn, float* xn, float* bCoefficients, int num_coeff, cbuf<float>* prevBuff, float coefficientScale = 1)
	{		
		auto swappedXn = chunk[(numSamplesPerChunk - 1) - n];
		*yn = *xn;
		*yn = bCoefficients[0] * swappedXn; // x[n] * b[0]

		for (int b = 1; b <= num_coeff - 1; b++) // x[n-1] * b[1] etc...
			*yn += prevBuff->ReadN(-b) * (bCoefficients[b] * coefficientScale);

		prevBuff->Put(*xn);
	}
};

I pretty much got it to work and then went to bed, it was 2 am. 

Apart from that, I went for a nice long run, took with me a gel (orange flavour) and my headphones and ran through what was first torrential rain, then sunshine and the only thing I missed thankfully was snow. I did, unfortunately, tweak my calf again so I'm hoping this won't be another stint in the docks and that a good night's rest will cure it. If it doesn't well, I'll put plan b into action.

This is what happens when you stop running (I took the train back 3 stops) and then got out and continued to run from there. So I ran about say 12km in total including the bit between stations on the way home but started cold off the train, which probably is what did it. I must learn!

So the main run was about 11km, took about 55 mins, 54:40 to be precise and was pretty good - I'd just woken up from said coding night and needed to get out.

 

I ran with an average pace of just under 5 mins per Km at 4:50 which was fine, I ran back to Citrix as I quite like that route and it gives me a kick every time I run back and see the old manor house. 

I also got a new laptop, well if you can call it that. I needed hardware support for OpenGL 4 and my old trust machine can't support that. It's pretty much like my laptop but lighter, prettier, touch-screenier, and its got twice the disk space, and it supports OpenGL 4. I still use my laptop though, because it still has everything I want in a laptop minus OpenGL4. Also, its easier to use on the train as it's got a mouse built into the keyboard, a-la-thinkpad!

I'm developing another prototype game which needs to demonstrate good architecture practices (decoupling, design patterns etc) but I had no time to do it this weekend, and times running out! I've started it though, its got loading levels, a UI and menu system, collision detection, rudimentary AI which is the next thing I need to fix - I basically want my NPCs to have a mind of their own!

Also, my player seems to be able to walk through walls randomly which is a bug in my finite state machine. 

And finally some interesting things I saw this week:

And this one cracks me up too!

I've also been working on developing a prototype game in C# that demonstrates good architectural skills and implementation.


Comments powered by CComment