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 PlotsWhen 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 CallbackThe 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 HandlingYou 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 FunctionThe 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.