Tutorial 3 - Audio programming with gtkIOStream

Moderator: flatmax

Post Reply
flatmax
Posts: 604
Joined: Sat Jul 23, 2016 11:39 pm

Tutorial 3 - Audio programming with gtkIOStream

Post by flatmax » Thu Jul 27, 2017 8:28 pm

gtkIOStream is a versatile software for signal processing and audio processing, amongst other features (such as GUI programming). It interfaces directly with ALSA and or jackd for low latency audio processing and routing. gtkIOStream also provides a nice port GUI for jackd.

This tutorial targets ALSA capture and SoX file writing. In this tutorial we will introduce the bare minimum requirements for ALSA capture and we will write the audio out to file so that we can test it worked. You can use this template to insert your own signal processing algorithms if you like.

Before we start, you have to have compiled and installed gtkIOStream. For certain components of gtkIOStream its compilation and installation isn't necessary, but for certain parts of audio processing it is necessary. Tutorial 0 and contents guides you through how to install and test the installation of gtkIOStream. Before you start this tutorial, you must have it installed first.

Our first step is to open the standard lightweight Raspberry Pi IDE with a blank file for us to code up in ... like so :

Code: Select all

geany ALSACapture.C
In the IDE, enter the following code (we will run through this code in execution order to understand what it is doing) :

Code: Select all

#include <ALSA/ALSA.H>
#include <iostream>
using namespace std;
using namespace ALSA;

#include <Sox.H>

int main(int argc, char *argv[]) {
  if (argc<2){
    cout<<"Usage:\n"<<argv[0]<<" audioFileName"<<endl;
    return -1;
  }
  const float duration=10.; // time to record in seconds
  const string deviceName="hw:0"; // the alsa device to open
  
  Capture capture(deviceName.c_str()); // open the capture device
  cout<<"opened the device "<<capture.getDeviceName()<<endl;
  cout<<"the device is in the "<<capture.getStateName()<<" state"<<endl;

  snd_pcm_format_t format;
  capture.getFormat(format);
  cout<<"format "<<capture.getFormatName(format)<<endl;
  float fs=capture.getSampleRate();
  cout<<"sample rate is "<<fs<<" Hz"<<endl;
  cout<<"channels "<<capture.getChannels()<<endl;

  // Open the audio file for writing
  Sox<short int> sox;
  int res=sox.openWrite(argv[1], fs, capture.getChannels(), pow(2.,(double)(snd_pcm_format_width(format)-1)));
  if (res<0)
    return SoxDebug().evaluateError(res);

  capture.setParams();
  if (!capture.prepared()){
    cout<<"should be prepared, but isn't"<<endl;
    return -1;
  }

  snd_pcm_uframes_t pSize;
  capture.getPeriodSize(&pSize);
  cout<<"period size "<<pSize<<endl;
  // Create our temporary buffer which we will read audio into before writing it out from
  Eigen::Array<short int, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> buffer(pSize,capture.getChannels());
  if ((res=capture.start())<0) // start the device capturing
    return ALSADebug().evaluateError(res);

  int N=(int)floor(duration*fs); // we will stop after roughly duration s
  while (N>0){
    capture>>buffer; // capture the audio data
    // do your signal processing here
    sox.write(buffer);
    N-=buffer.rows();
  }
  capture.drop(); // halt the ALSA capture device
  
  sox.closeWrite();
  return 0;
}
The first thing we do is enter our main loop and we check to see that the user has specified the audio file name for saving to. It looks like so :

Code: Select all

int main(int argc, char *argv[]) {
  if (argc<2){
    cout<<"Usage:\n"<<argv[0]<<" audioFileName"<<endl;
    return -1;
  }
You see if the user doesn't specify the command (argv[0]) and the file name (argv[1]) which makes two arguments, then we need to tell them how to run the program.

Next we setup some constants the first constant is the duration to record for (roughly 10 seconds) :

Code: Select all

  const float duration=10.;
  const string deviceName="hw:0";
and the second is the ALSA device we wish to open. You can see all of the available devices using the arecord -l and arecord -L commands.

Next we have included the ALSA header and the Sox headers, including the iostream header for convenient message printing :

Code: Select all

#include <ALSA/ALSA.H>
#include <iostream>
using namespace std;
using namespace ALSA;

#include <Sox.H>
We also use the namespaces for convenience - that gives us access to EVERYTHING in the std and ALSA namespaces, which don't clash by design.

Now that we have access to the gtkIOStream ALSA classes we open the capture device and print the name to screen and we also find out what state the capture device is in :

Code: Select all

  Capture capture(deviceName.c_str()); // open the capture device
  cout<<"opened the device "<<capture.getDeviceName()<<endl;
  cout<<"the device is in the "<<capture.getStateName()<<" state"<<endl;
  
OK, at this point, we have the capture device open and the hardware parameters (hwparam in ALSA talk) are setup to defaults (which you can see in this file).

We will print out the format, sample rate and channels next :

Code: Select all

  snd_pcm_format_t format;
  capture.getFormat(format);
  cout<<"format "<<capture.getFormatName(format)<<endl;
  float fs=capture.getSampleRate();
  cout<<"sample rate is "<<fs<<" Hz"<<endl;
  cout<<"channels "<<capture.getChannels()<<endl;
Nothing too special here, the code is pretty clear.

We want to open the SoX file, which we have seen in the last tutorial 1 :

Code: Select all

  Sox<short int> sox;
  int res=sox.openWrite(argv[1], fs, capture.getChannels(), pow(2.,(double)(snd_pcm_format_width(format)-1)));
  if (res<0)
    return SoxDebug().evaluateError(res);
Again, we calculate the maximum sample value using knowledge of the number of bits per sample - we should strictly check to see that the sample type is signed here, or else we are clipping for very large values - however the default sample type is signed.

Before we can start playing we want to know the period size and also we want to put the ALSA device into the prepared state, which we do like so :

Code: Select all

  capture.setParams();
  if (!capture.prepared()){
    cout<<"should be prepared, but isn't"<<endl;
    return -1;
  }
The setParams method attempts to write the hardware params to the device and we check to see that we are in the expected prepared state. If not then we print an error and return. We should strictly use the ALSADebug::evaluateError method to get meaningful information on why there was an error, which we will demonstrate in a few lines below.

Great - so now the capture device is open and in the prepared state ... all we need to do is get some memory for audio reading/writing, start the device and loop writing the captured audio to file.

First we print out the period size :

Code: Select all

  snd_pcm_uframes_t pSize;
  capture.getPeriodSize(&pSize);
  cout<<"period size "<<pSize<<endl;
We then tune the memory buffer to be the same size as an ALSA period :

Code: Select all

 Eigen::Array<short int, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> buffer(pSize,capture.getChannels());
Here we use the Eigen Array type because perhaps we will want to do some signal processing on the audio buffer. You will notice it has period sized number of rows and enough rows to hold all of the audio channels.

Then we start the ALSA device to capture audio :

Code: Select all

  if ((res=capture.start())<0) // start the device capturing
    return ALSADebug().evaluateError(res);
Here we give a contextual error report if there is a failure and return that error.

Finally we do our capture write loop where first we work out how many frames (samples per channel) we need to grab (N) which is the number of samples per second (fs which is the sample rate in Hz) multiplied by the duration in seconds (SI units right !) :

Code: Select all

 int N=(int)floor(duration*fs);
  while (N>0){
    capture>>buffer; // capture the audio data
    // do your signal processing here
    sox.write(buffer);
    N-=buffer.rows();
  }
We enter the while loop, capture audio into the buffer (using the operator>>), write the audio to file using sox and decrement the required number of frames by the number of rows captured.

So you see it is pretty trivial at this point, simply capture to a buffer, write to file or do some signal processing first before writing.

Finally, we drop and stop the ALSA capture device so there are no over runs (a term used for not reading a real time buffer quickly enough and it "over flowing"). We also close the audio file and return 0 on exit.

Code: Select all

  capture.drop(); // halt the ALSA capture device
  
  sox.closeWrite();
  return 0;
}
OK - all ready now to compile, run and test !

We compile it with the following command :

Code: Select all

g++ `pkg-config --cflags --libs gtkIOStream` -o ALSACapture ALSACapture.C
Using the ALSACapture file as the output and similar name for the source code we entered into our IDE (or text editor).

We run like so - capturing a wav file in this instance (it could be almost any other audio file format) and this is what you should see :

Code: Select all

$ ./ALSACapture /tmp/test.wav
opening the device hw:0
opening the device hw:0
		func: fillParams
		func: setAccess
		func: setFormat
		func: setChannels
		func: setSampleRate
opened the device 		func: getDeviceName
hw:0
the device is in the 		func: getStateName
OPEN state
format S16_LE
sample rate is 48000 Hz
channels 2
		func: setHWParams
		func: getSWParams
		func: setSWThreshold
		func: setAvailMin
		func: setSWParams
period size 256
		func: drop
		func: running
		func: close
		func: drop
		func: running
PCM::drop can't drop, not running
There is a lot of detail printed out, however all of the significant and desired information is present. You will notice that we are using by default a signed 16 (S16_LE) sample format, with a sample rate of 48 kHz and 2 channels. The period size by default is 256 samples which is around 5 ms. This is nice and safe and should pose little problems, may even be able to compress and encode in real time by changing the file name to something like /tmp/test.ogg or flac or whatever you want - that would take some experimentation.

We test by listening to it using aplay

Code: Select all

aplay /tmp/test.wav
and you can open the file in audacity and see what it looks like, it should look like stereo waveforms :
test.wav.png
ALSACapture audio file opened in audacity.
test.wav.png (49.71 KiB) Viewed 2416 times
If you don't record anything (waveforms are zero) then you have to check your alsamixer. In the case of the Audio Injector cards, to capture from the input RCA lines and not the microphone, your capture alsamixer page should look like this :
Image

and your playback page should look like this :
Image
Checkout the next kickstarter campaign - the Ultra 2 : https://kck.st/2xwM3DL
Please review the Zero sound card on Amazon USA : https://www.amazon.com/dp/B075V1VNDD
---
Check out our new forum on github : https://github.com/Audio-Injector

niyer
Posts: 7
Joined: Thu Aug 17, 2017 9:42 am

Re: Tutorial 3 - Audio programming with gtkIOStream

Post by niyer » Thu Aug 17, 2017 9:56 am

Thank you for the tutorials. They helped create a great starting point for my project and also helped test my hardware and software setup.
The only two steps that I had to do in addition to your instructions was to install sox and GTK2. I used the sudo apt-get command to install them and got back on to the track.

Nathan

flatmax
Posts: 604
Joined: Sat Jul 23, 2016 11:39 pm

Re: Tutorial 3 - Audio programming with gtkIOStream

Post by flatmax » Fri Aug 25, 2017 8:28 pm

Oh great ! Thanks for the feedback.

Can you please tell me at which point you were missing them ? Was it part of these tutorials ?

thanks
Matt
Checkout the next kickstarter campaign - the Ultra 2 : https://kck.st/2xwM3DL
Please review the Zero sound card on Amazon USA : https://www.amazon.com/dp/B075V1VNDD
---
Check out our new forum on github : https://github.com/Audio-Injector

niyer
Posts: 7
Joined: Thu Aug 17, 2017 9:42 am

Re: Tutorial 3 - Audio programming with gtkIOStream

Post by niyer » Thu Sep 14, 2017 2:21 pm

Hi Matt,
Yes, this was part of the tutorials. I followed tutorial 0 step by step and found that I had to do these two additional steps to make it work.
It is not a big deal, wanted to mention it so that it can help others.

Thanks,
Nathan

Post Reply

Who is online

Users browsing this forum: No registered users and 5 guests