The TLV320AIC23B is a great little audio CODEC and DAC, which like most hi-spec chips has n different configurations and takes 2n hours to set up! So, in the usual mbed style I’ve tried to simplify the number of configurations, so that you’re left with something that just works. Luckily, mbed are releasing a new demo board through RS Components, which should be available to buy in the not too distant future. This has a built in TLV320 with all the extra hardware done for you.
Despite that, there are still a couple of things to set up. Why? Because audio files come in a variety of flavours. So what does the mbed user need to specify before using this device? Well I’ve whittled it down to the following:
• word width
• mono/stereo mode
• sample rate
• devices to power
The I2S port uses a FIFO buffer for both input and output. Each FIFO is 8 x 32-bit words deep and at a certain depth an interrupt is triggered. Upon interrupt it’s up to the user to do something with the audio data. Note the FIFOs have to be cleared in order to escape the interrupt service routine.
How to use the API¶
The TLV320 library has a big API, but it easily splits up into 3 distinct sections:
- Initialisation and setup
- Modes and settings
Initialisation and setup¶
This requires you to power up the TLV320, which comes with two options, power everything OR power individual embedded devices.
- Set the transfer protocol format i.e. word width (I suggest 16bit) and number of channels (MONO(1) , STEREO(0)).
- You then need to set sampling rate in hertz (8000Hz, 8021Hz, 32000Hz, 44100Hz, 48000Hz, 88200Hz, 96000Hz)
- Lastly, attach an interrupt handler.
If you don't do these, the default settings are:
- word width = 16bit
- number of channels = STEREO
- interrupt handler = NONE
With receive mode enabled, if you call the start() function without having attached an interrupt handler with a read() function, the code will hang until you empty the I2SRXFIFO. Read on.
Attach a function to the interrupt handler via the attach() function, be careful to know what sort of function you're attaching. If it's a function without a class, or a static member function (i.e. a function defined within a class but there is only ever one instance of it because it is declared with the static label) then use:
myTlv320.attach(&myFunction); //OR myTlv320.attach(&myClass::myStaticMemberFunction);
If, however, the function belongs to a class and it is 'normal' in the sense that it is not defined with the static qualifier use:
To start interrupts simply call the start() function, which accepts a parameter to define the type of interrupt you wish to receive, namely, NONE (not useful), RECEIVE, TRANSMIT, BOTH. I suggest you do not use BOTH, because you then have to figure out which FIFO triggered it, via the status() function and it usually turns out that if you're taking data from the I2SRXFIFO DSP-ing it and then sticking it on the I2STXFIFO, you're generating the majority of the interrupts just by placing data in the I2STXFIFO. The receive FIFO triggers at a depth of 4 words, the transmit, when it's empty.
To stop interrupts, call stop().
Modes and settings¶
These include the input/output volume control, a mute function and a bypass function. Bypassing essentially connects the input to the output, but still via the ADC and DAC, so that you can still control volume
|TLV320 (PinName sda, PinName scl, int addr, PinName tx_sda, PinName tx_ws, PinName clk, PinName rx_sda, PinName rx_ws)|
object defined on the I2C port.
|void||power (bool powerUp)|
|void||power (int device)|
function default = 0x07, record requires 0x02.
|void||format (char length, bool mode)|
Set I2S interface bit length and mode.
|int||frequency (int hz)|
Set sample frequency.
|void||start (int mode)|
Start streaming i.e.
Stop streaming i.e.
|void||write (int *buffer, int from, int length)|
Write [length] 32 bit words in buffer to I2S port.
Read 4 x (32bit) words into rxBuffer.
Attach a void/void function or void/void static member function to an interrupt generated by the I2SxxFIFOs.
|template<typename T >|
|void||attach (T *tptr, void(T::*mptr)(void))|
Attach a nonstatic void/void member function to an interrupt generated by the I2SxxFIFOs.
|int||inputVolume (float leftVolumeIn, float rightVolumeIn)|
Line in volume control i.e.
|int||outputVolume (float leftVolumeOut, float rightVolumeOut)|
Headphone out volume control.
|void||bypass (bool bypassVar)|
Analog audio path control.
|void||mute (bool softMute)|
Digital audio path control.
This is really what the mbed is about – projects! So here are a couple.
1 - Simple playback¶
By creating a circular buffer, it is possible to load data continually into some flash from, say, an SD card and then feed it, 8 words at a time to the I2S port. If you have CD quality tracks, you definitely get CD quality playback! I used several of my own .wav files with 16bit stereo at a sample rate of 32kHz, so just sub-CD quality, but it still sounds good.
I used a neat little trick to parse the .wav file header and strip out the necessary information to set up the I2S port and TLV320, so that it would play any track automatically without me having to set it up each time. Actually, the TLV320 contains an I2S object, so that all you have to do is send your settings to the TLV320 and that deals with all of the complicated I2S configurations for you :-)
If you've never encountered circular buffers, it's basically a buffer which loops back on itself (like in the diagram), such that you can continually write to it and read from it. If this is not to your liking, you could use a system of double buffering, or even triple buffering, which is where you constantly switch between buffers in synchrony, such that you write to one whilst you read from another. I prefer the circular buffer though, just because I think it's elegant :D This does, however, require a bit of pointer-fun, but if you use the array data type there's no need for any of that. My program's a bit big to post so go here
Okay, playback is a bit samey. My iPod can do that. What can my iPod not do? Well it can’t do text-to-speech synthesis. Sound tough? Well, yes, but there’s a nice little cheat which you can use to get around this and strictly speaking this next project is not text-to-speech, but more numbers-to-speech.
2 - FTSE100 readout¶
You may have read some of my previous posts, in particular one about displaying the FTSE100 on an alphanumeric display. I decided seeing the FTSE100 at work every day was not enough to satisfy my addiction to real-time data, I needed to cover more senses! So I got the old FTSE100 display program and after a little jiggery-pokery I managed to rearrange the program. It parses the value of the current FTSE100 into six digits and a decimal point and calls an audio file corresponding to the value of the digit or decimal point. All in all I recorded 12 files; ten for the numbers 0-9, one for the decimal point and one to introduce the FTSE100. Neat!
The program takes advantage of a PHP script, which I uploaded to a bit of web-space. It looks at this website and in the top right hand corner, there's a little number, which is the current value of the FTSE100 delayed by 15mins. Every time you poll the website where the script is stored, it in turn polls the Yahoo Finance website and then parses the source code for this number, which is possible because the number is wrapped inside two distinct HTML id tags. So the mbed then parses the source code of my own webpage, but only needs to read one line, so programming-wise it's easier for me. As I wrote in the previous paragraph, it splits the number up and loads the appropriate files, which I named 0.wav, 1.wav, 2.wav ... 9.wav and then there's one for the decimal point and one for the introduction. Watch the video!
Again, you can find my code here
3 - Recording¶
So, I've pretty much covered reading from files, but I haven't looked at writing them. The TLV320 has a microphone and line-in channel, but the audio board which I'm using hasn't exposed the microphone input, so I'm forced to use the line-in. This requires a powered input, say my iPod, mobile phone, pc, anything that could be used with a pair of headphones really. I've also decided that it may be easier to write to a USB stick, so I can then transfer my audio files onto my pc without too much sd micro to usb converter bother. The internal RAM on the mbed is far too small to store an audio file and then write it to memory after recording has finished, so I run a quick diagnostic, using a Timeout object and a terminal window to check my write speed. It turns out that I'm limited to a sampling rate of 32kHz, but then I find every 66 writes of 512 bytes the USB system hangs for 0.2 seconds - I assume this is to do with internal memory allocation/buffer shunting etc. Anyway, in this amount of time I would fill 40% of my total RAM! Okay, that's not a problem, but I have to be able to empty that onto the USB stick before the next long wait as well as all the data which streams in during the periods without the hang. But the write speed is just as fast as my input data speed, so I can't run at 32kHz, I'm limited to 8kHz...bother! Nonetheless, this has the potential to still sound better than some of Dad's old BeeGees cassettes (yes, he still listens to tape casettes!).
My record data will come from my pc, I can run a BBC Radio1 pop-out and connect the audio output from the computer to the audio board via a male-male TRS Jack.
Something, which I'm going to have to do is populate a header, full of useful information, so I can play the files on the computer. After half an afternoon scouring the web for .wav header information, I end up with a bit of code which can write the header from just three parameters:
- Sampling rate
- Word width
- Number of channels
I found that the rest of the data is either fixed for this kind of file, arbitrary or just a function of those three parameters.
The final program can be found here.