Tuesday, July 29, 2008

Display Cursor Coordinates

A simple callback function that prints the current cursor location in plot coordinates into the plot window in a user specified location/format/colour.

DisplayCursorLocation

% cursorLocation - WindowButtonMotionFcn displaying cursor location in plot
%===============================================================================
% Description : Display the current cursor location within the bounds of a
% figure window. Assigned as a WindowButtonMotionFcn callback
% function. Only updates when mouse is moved over plot contents.
%
% Parameters : obj - Figure originating the callback
% event - not used (but required)
% location - Location within plot window for text. Can be
% 'BottomLeft', 'BottomRight', 'TopRight', 'TopLeft'
% or a [1x2] array of XY location
% format_str - A sprintf format string that will accept 2 float
% parameters. ie 'X: %.3f, Y: %.3f'
% text_color - either a color character (ie 'r') or a RGB
% triplet (ie [1.0 1.0 0.5])
%
% Return : None
%
% Usage : Assign to a Figure WindowButtonMotionFcn callback:
% set(fig_handle, 'WindowButtonMotionFcn',
% @(obj, event)cursorLocation(obj, event, 'BottomLeft',
% 'X: %.3f, Y: %.3f', 'r')
%
% Author : Rodney Thomson
% http://iheartmatlab.blogspot.com
%===============================================================================
function cursorLocation(obj, event, location, format_str, text_color)

The cursorLocation function is assigned as the WindowButtonMotionFcn for a figure. Any time the mouse is moved over the specified figure, the callback function will be executed.

The callback function retrieves the cursor location in plot axes coordinates and uses the supplied sprintf format to produce a text label which is printed in a specific location in the plot. This location can be a preset value or an arbitrary [X,Y] coordinate.

Example usage:
t = linspace(-5,5);
y = sinc(t); f = figure; plot(t, y, 'r');
set(f, 'WindowButtonMotionFcn', ...
@(obj, event)cursorLocation(obj, event, 'BottomLeft', ...
' X: %.3f\n Y: %.3f', 'r')

If you wanted to avoid setting the WindowButtonMotionFcn callback yourself, you could use the following wrapper function:
function displayCursorLocation(figure_handle, location, format_string, text_color)

set(figure_handle, 'WindowButtonMotionFcn', ...
@(obj, event)cursorLocation(obj, event, location, format_string, text_color));
end

Tuesday, July 22, 2008

Recursive Directory Function Execution

A simple function that provides a large amount of flexibility in its use.

DirectoryRecurse

% directoryRecurse - Recurse through sub directories executing function pointer
%===============================================================================
% Description : Recurses through each directory, passing the full directory
% path and any extraneous arguments (varargin) to the specified
% function pointer
%
% Parameters : directory - Top level directory begin recursion from
% function_pointer - function to execute with each directory as
% its first argument
% varargin - Any extra arguments that should be passed
% to the function pointer.
%
% Call Sequence : directoryRecurse(directory, function_pointer, varargin)
%
% IE: To execute the 'rmdir' command with the 's' parameter over
% 'c:\tmp' and all subdirectories
%
% directoryRecurse('c:\tmp', @rmdir, 's')
%
% Author : Rodney Thomson
% http://iheartmatlab.blogspot.com
%===============================================================================
function directoryRecurse(directory, function_pointer, varargin)

contents = dir(directory);
directories = find([contents.isdir]);

% For loop will be skipped when directory contains no sub-directories
for i_dir = directories

sub_directory = contents(i_dir).name;
full_directory = fullfile(directory, sub_directory);

% ignore '.' and '..'
if (strcmp(sub_directory, '.') || strcmp(sub_directory, '..'))
continue;
end

% Recurse down
directoryRecurse(full_directory, function_pointer, varargin{:});
end

% execute the callback with any supplied parameters.
% Due to recursion will execute in a bottom up manner
function_pointer(directory, varargin{:});
end

The directoryRecurse function finds all directories below the supplied directory and executes the supplied function with the full directory path as the first argument. Any extra arguments supplied to directoryRecurse are passed onto the function supplied in function_pointer.

Its a hard concept to explain with words, so here are a few useful examples:

% add current directory and any sub-directory to the Matlab search path
directoryRecurse(pwd, @addpath);

% delete all contents of 'c:\tmp', requires passing the 's' flag to the rmdir function
directoryRecurse('c:\tmp', @rmdir, 's')

Tuesday, July 15, 2008

Sound Card Spectral Analyser GUI

Glancing at the title of this post you might be mistaken for thinking I have repeated my previous post! This is not the case.

SoundcardSpectralAnalyserGui

This GUI (developed using GUIDE) took the SoundcardSpectralAnalysis functionality and wrapped it in a Matlab class. An object of this class was then utilised by the GUI in order to display real time analysis of acoustic data sampled off the system sound card.

Object Oriented Matlab

A Matlab class named SoundcardSpectralAnalyser was developed. It has a constructor, and start(), stop() and set() methods.

Unfortunately, as I am not yet using Matlab 2008a, I do not have access to its wonderful new OO interface. I will not go into too much detail regarding the older OO mechanic because eventually it will be outdated... And it is nothing to be celebrated really. (Hmmm maybe i should register ihatematlab.blogspot.com)

Constructor
The responsibility of the constructor is to initialise any member variables and define the returned structure as a Matlab class of type 'SoundcardSpectralAnalyser'.

Note that the constructor calls out to the set() method to initialise parameters from the variable length argument list. This has to be called AFTER the class type declaration to ensure the correct set method is used.
function this = SoundcardSpectralAnalyser(time_plot, freq_plot, varargin)

% Initialise default parameters if not supplied
this.Fs = 44000;
this.n_bits = 16;
this.n_channels = 2;
this.update_rate = 5;

this.time_plot = time_plot;
this.freq_plot = freq_plot;

this.audio_recorder = [];

this = class(this, 'SoundcardSpectralAnalyser');

% Set parameters as supplied
this = set(this, varargin{:});

end

Start()
The start method extracted the initialisation and audiorecorder starting functionality from the SoundcardSpectrumAnalysis function. Separating this ensures that an object of SoundcardSpectrumAnalyser can be constructed at one time but not started until a later point in time. Also, subsequent audiorecorder objects will be using the current analysis parameters (sample rate, sample size, number of channels and update rate).


The start() method also contains the TimerFcn callback responsible for updating the supplied plots. This is functionally unchanged from the script version.
function this = start(this)

% Setup the audiorecorder which will acquire data off default soundcard
this.audio_recorder = audiorecorder(this.Fs, this.n_bits, this.n_channels);

set(this.audio_recorder, 'TimerFcn', {@audioRecorderTimerCallback, ...
this.time_plot, this.freq_plot});
set(this.audio_recorder, 'TimerPeriod', 1/this.update_rate);
set(this.audio_recorder, 'BufferLength', 1/this.update_rate);

record(this.audio_recorder);
end

Stop
()
The stop method is responsible for stopping the audiorecorder object if it had been created.

function this = stop(this)  

if (~isempty(this.audio_recorder))
stop(this.audio_recorder);
end
end

Set()
The set method takes in a variable length argument list
. This list comprises of Value/Key parameter pairs for setting the sample rate, sample size, number of channels and update rate. All pairs can be set simultaneously or 1 at a time.

I was considering using the inputparser class to do this behaviour, but unfortunately I could not figure out how I could set only 1 of the parameters externally without the remaining parameters in the struct being returned as the specified defaults. Also, it was not available in my oldest version of Matlab (R14 SP2).
function this = set (this, varargin)

if (mod(length(varargin), 2) ~= 0)
warning('Parameters must be supplied in Key/Value pairs.');
return;
end

for i_param = 1:2:(length(varargin) - 1)

switch varargin{i_param}
case 'Fs'
this.Fs = varargin{i_param+1};
case 'SampleSize'
this.n_bits = varargin{i_param+1};
case 'Channels'
this.n_channels = varargin{i_param+1};
case 'UpdateRate'
this.update_rate = varargin{i_param+1};
otherwise
warning('Unknown parameter : %s\n', varargin{i_param});
end
end
end

GUI Development in GUIDE


I developed the GUI using Matlab's inbuilt GUI editor known as GUIDE (Graphical User Interface Development Environment). This provides a 'designer' like layout tool which will create the required .fig and .m files for your GUI. It also automatically creates hooks for object callbacks in the .m file for your convenience.

I will not go into too much detail regarding the GUIDE tool. I'd personally recommend firing up the editor (type 'guide' at the Matlab Command Window) and just throw some things on there, put some fprintf() statements in the generated callbacks and go click crazy. I will go into a couple of things that annoy about GUI development in Matlab in particular.

Disclaimer: This GUI was developed using R14 SP2... Guide may have been improved in later releases. I'm yet to investigate and the issues I have found with it may have been fixed!

Figure Resizing
Ok, the default behaviour of a Matlab GUI is to disallow resizing of the main figure window. This is convenient for a Matlab GUI developer... not so much for a Matlab GUI user. Particularly if there a plots involved as users with high resolution screens may wish to take advantage of their expensive toys.

So.. you bring up the Property Inspector for the main figure and set the Resize property to 'on'. Run the GUI and *yay* you can resize the window. But all the contents stay wedged in the bottom left. Hmmm thats no better.

Now, go into the Tools->GUI Options... menu and change the resize behaviour to 'Proportional'. Run the GUI and observe the behaviour.

Hey, thats a little bit better. Any Axes you have in the GUI are being proportionally resized and overall, things look good. However, the proportional resize affects ALL objects - pushbuttons, panels. Things can start to look a bit odd and often careful laying out of objects turns into a complete mess.

So what is the alternative?

Define a ResizeFcn on the main figure window. This is then called when the user resizes the GUI window. From within this callback you can query the current figure position and size and shuffle around your objects manually.

This process is quite tedious but unfortunately required for full control of resize behaviour (There appears to be no 'anchoring' of panels/objects).

Check out SoundcardSpectralAnalyser_GUI_ResizeCallback in the main GUI .m file for an example of the code required to achieve this control.

The desired behaviour of my Resize function is to keep the Parameters panel at the bottom of the figure, keeping its height constant but adjusting the width to match the figure. The remaining figure space is divided equally for the time and frequency domain plots (ensuring there was enough room around the axes for labels).

I did not put any limitations in the Resize function to enforce a minimum figure size as at 30 lines it was getting long enough.

Figure Axes
It was annoying having to remember to pad the size of the Axes to ensure sufficient size for the tick labels/text labels. Ideally the size specified by the Axes would be the maximum bounds containing titles/xlabel/ylabel/colorbars etc. Matlab should be smart enough to dynamically resize the drawn chart area internally to cope with this (as it does with a figure window currently.. actually that gives me an idea.. embedding a figure into a GUI for this very reason. Stay tuned).

Orphaned Objects
Something I noticed whilst developing the GUI was that I was often getting orphaned instances of my SoundcardSpectralAnalyser object (and subsequently the audiorecorder object) when there was an error and the GUI did not close properly.

When I tried to run 'clear classes' I received a warning that X instances of SoundcardSpectralAnalyser exist and classes could not be cleared. Doing a findall(0, 'Type', 'SoundcardSpectralAnalyser') yielded no results, hence there is no way of clearing these.

This may be a bug that has been fixed in more recent versions.

Overall, I feel that Matlab GUIs have a long way to go from that implemented in R14 SP2. I will have a look at 2007b (most recent installed version that I use) and see if things have improved. With care, reasonable GUIs can be developed in Matlab but most of the effort will go into tasks that should really be much simpler.

Tuesday, July 1, 2008

Sound Card Spectral Analysis

As my first post on this blog I thought I would introduce a very simple function which exploits some of Matlab's high level data acquisition and plotting abilities.

Although this blog's title is 'iheartmatlab' I will also explore areas about Matlab that I don't like so much.

Ok, onto the first code example:

SoundcardSpectralAnalysis

%===============================================================================
% Description : Acquire acoustic data from default system soundcard and plot
% in both time and frequency domain.
%
% Parameters : Fs - Acquisition sample frequency [44000] Hz
% n_bits - Sample size [16] bits
% n_channels - Number of channels to acquire
% from sound card [2]
% update_rate - Polls sound card for data this
% many times per second [5] Hz
%===============================================================================
function soundcardSpectralAnalysis(Fs, n_bits, n_channels, update_rate)
As per the description this function will continuously acquire data from the soundcard at the specified sample frequency, sample size and update rate for however many channels your soundcad supports.

Default Parameters

One thing I don't like about Matlab is the lack of an efficient manner to define the value of default parameters. I have had to resort to the following to initialise my parameters:
% Initialise default parameters if not supplied
if (~exist('Fs', 'var'))
Fs = 44000;
end
if (~exist('n_bits', 'var'))
n_bits = 16;
end
if (~exist('n_channels', 'var'))
n_channels = 2;
end
if (~exist('update_rate', 'var'))
update_rate = 5;
end
The code checks whether a variable exists within the workspace; and if it does not, it creates it with the default value. There are other possible methods that could have been utilised such as:

if (nargin < 4)
update_rate = 5;
if (nargin < 3)
n_channels = 2;
if (nargin < 2)
n_bits = 16;
if (nargin < 1)
sample_frequency = 44000;
end
end
end
end
Although this method uses slightly less lines of code, the level of nesting makes it a bit hard to understand on first glance the purpose of the code. And the approach implemented is at least insensitive to changes in the order of parameters (which is unlikely... but you never know).

My ideal dream solution for default parameters would be similar to C++ (the other language with which I have some experience):

function soundcardSpectralAnalysis(Fs = 44000, n_bits = 16, n_channels = 2, update_rate = 5)

If a parameter does NOT have an assignment against it, then its deemed to be a required parameter.

Initialising Plots

When you are producing plots in Matlab and are going to be continuously updating the contents of the plot, typically overdrawing or updating the previous result (ie updating an line spectrum or vessel track) then I would recommend initializing a plot with your desired visual properties and then updating only the raw data values contained by the plot.

Too often i've seen:
tic
figure
for i = 1:100
cla
plot(i, i, '.')
drawnow
end
toc
Elapsed time is 4.899789 seconds.
A much more efficient alternative:
tic
figure;
point_plot = plot(nan, nan, '.');
for i = 1:100
set(point_plot, 'XData', i);
set(point_plot, 'YData', i);
drawnow
end
toc
Elapsed time is 1.086440 seconds.
So the SoundcardSpectralAnalysis function initialises the time and frequency domain plots and sets up axis bounds / labels:
plot_colors = hsv(n_channels);

% Initialise plots, one above each other in a single figure window
figure;

% Time Domain plot
subplot(2,1,1)
hold on
for i_channel = 1:n_channels
time_domain_plots(i_channel) = plot(nan, nan, ...
'Color', plot_colors(i_channel, :));
end
xlabel('Sample')
ylabel('Counts')

y_max = 2^(n_bits-1);
ylim([-y_max y_max]);

% Frequency Domain plot
subplot(2,1,2)
hold on
for i_channel = 1:n_channels
freq_domain_plots(i_channel) = plot(nan, nan, ...
'Color', plot_colors(i_channel, :));
end
xlabel('Frequency (Hz)')
ylabel('dB re 1 count/sqrt(Hz)')
xlim([0 Fs/2])
ylim([0 70])

Audio Recorder

Now we come to the guts of the data acquisition, the Matlab inbuilt audiorecorder.
% Setup the audiorecorder which will acquire data off default soundcard
audio_recorder = audiorecorder(Fs, n_bits, n_channels);

set(audio_recorder, 'TimerFcn', {@audioRecorderTimerCallback, ...
audio_recorder, ...
time_domain_plots, ...
freq_domain_plots});
set(audio_recorder, 'TimerPeriod', 1/update_rate);
set(audio_recorder, 'BufferLength', 1/update_rate);

% Start the recorder
record(audio_recorder);
The audiorecorder is a nice simple high level abstraction allowing us to retrieve data from a soundcard. However there is limited control as to how often and how much data is retrieved from the sound card.

Ideally for this application we would determine the number of samples to be read from the sound card (sample_frequency/update_rate) and enter a loop reading this many samples each time. However audiorecorder does not contain this functionality. The only option is to specify a timer callback function and its timer period.

In theory this should work perfectly, however in the real world, timers don't get called EXACTLY every 0.2 seconds (or however long their interval). Depending on CPU load and interrupt timings it can vary.

The effect on audiorecoder is that often you will receive too much / too little data when you query it for the currently recorded data. Unfortunately i could not figure out a simple work around. So this function wil occasionally 'skip' and update and include that data into the next frame's analysis. Sorry folks!

Now.. what makes this code work? Simple its this:
set(audio_recorder, 'TimerFcn', {@audioRecorderTimerCallback, ...
audio_recorder, ...
time_domain_plots, ...
freq_domain_plots});
This says that for each time period as configured, execute the 'audioRecorderTimerCallback' function and supply to it these three parameters.

By then starting the recorder you are beginning recording and starting the timer.

Audio Recorder Callback

The callback function we assigned to the audiorecorder will get called approximately ever 1/update_rate seconds. I wont go into too much detail regarding the callback as this post is long enough as is.

The function initially queries the audiorecorder for some of its properties required for accurate frequency analysis.

It then stops the recorder, retrieves the recorded data before starting the recorder again.
% stop the recorder, grab the data, restart the recorder. May miss some data
stop(obj);
data = getaudiodata(obj, data_format);
record(obj);
It then calculates the power spectrum of the audio data and updates the XData and YData fields of the appropriate time / frequency domain plots before forcing a drawing update of the plots.

Error Handling

You might have noticed that the majority of the code in the Audio Recorder callback function is contained within a try block. This is for 2 reasons:

1) If a systematic error is occuring within the function, then it will continue to occur within the function. Each time that function is called. Which is every time the Audio Recorder timer function is triggered. And because at the command line you do not have access to the soundcardSpectralAnalysis workspace, you cannot stop the timer. Your only solution is to close Matlab.
2) You wish to stop the spectral analysis? Well, just closing the figure window will acheive this.

The corresponding catch block does the following:
catch exception
% Stop the recorder and exit
stop(obj)
rethrow(exception)
end
Most important line is "stop(obj)". This stops the recorder and hence the timer function. The error is then rethrown to alert the user to the issue.

In the case of 1) above, you will be able to debug the code if you were extending its functionality withough having to restart Matlab often. In the case of 2), closing the figure window means that when it attempts to update the plot data fields, they do not exist, causing an error to be thrown. Hence the function can be stopped nicely.

Interesting Issue with Audio Recorder Callback Function

The more observant readers might say:
"Why supply "audio_recorder" to the audioRecorderTimerCallback. Particularly as the audio_recorder is the initiater of the callback available as the "obj" variable, and also because audio_recorder is not even used within the callback function whatsoever!"

And I would completely agree with you.

However. If you remove "audio_recorder" from the callback function parameter list, then the timer is never started and never executed:
set(audio_recorder, 'TimerFcn', {@audioRecorderTimerCallback, ...
time_domain_plots, ...
freq_domain_plots});

function audioRecorderTimerCallback(obj, event, ...
time_domain_plots, freq_domain_plots)

!!!!FAILS!!!!
It is particularly puzzling seeing as though audio_recorder is not used anywhere within the callback function. This can be illustrated by calling "clear('audio_recorder')" at the start of the function. It still functions as per expected.

It might be a bug, but possibly me misunderstanding the behaviour of the callback functions. If anyone has any suggestions, feel free to leave them in the comments below.

Anyhow, thanks for reading my long post about a very simple function. Hopefully its highlighted some interesting aspects of Matlab that you may not have been too familiar with.

I aim to provide a new sample each week along with a little explanation.