InternetRadio, prelude

Prelude: feasibility

Before starting a project, it is useful to think about which are the hard parts in that project. It helps to do a few smaller test projects first to learn about these parts, and to reduce the unknowns in the project.

In this project, the core parts are (1) playing an MP3 stream, and (2) presenting a user interface using a screen and a set of buttons.

For (1), the sequence of operation is as follows:

  • make a TCP connection to a streaming server
  • as the packets come in, they have to be fed into an MP3 decoder
  • the MP3 decoder produces a stream of samples, which have to be transformed into an analog audio signal
  • the audio signal then has to be amplified
  • the amplified signal is then sent into a speaker.

For (2), initially the serial console works just fine. However, for a standalone device, an actual screen, and actual buttons are needed. For the screen, this requires implementing the communication protocol. Buttons are regular GPIO pins, no surprises there.

Having no prior experience with DACs and audio in general, a few experiments were in order to eliminate the unknowns and see if I would be able to make all the parts of the system work. These are the experiments I did:

  1. DAC / SPI
  2. Hearing the tone
  3. Upgrade to ARM
  4. Actual samples
  5. MP3 decoding
  6. Screen

1. DAC / SPI

The first unknown is controlling the DAC chip. The MCP4921 is controlled over SPI; the easiest way to play with that was by hooking it up to an Arduino.

The MCP4921 is a 12-bit DAC, which means it accepts sample values between 0 and 4096. These values are translated to voltages between 0V and 5V. For audio, the samples swing above and below a midpoint; in this case (12 bits) this means that we’ll have the midpoint at 2048 (= 2.5V).

In order to test the generation of sound, then, we can generate samples for a sine wave, using a simple python script. The script steps around a circle in 1-degree steps, and scales the resulting value between 0 and 4000:


from math import sin, trunc

angle = 0
for i in xrange(0,360):
  
  angle = angle + 0.01745;
  sinVal = sin(angle);

  # -1 < sinval < 1
  # the following will produce a value between 0 and 4000
  sample = trunc((sinVal * 2000) + 2000);

  print "{0:4}\t{1:7}".format(i, sample)

The resulting table of values is then used in an Arduino sketch to send values over SPI to the MCP4921. There are plenty of code examples of how to interface the MCP4912 to an Arduino; the code below was adapted from Bart Venneker’s example to send the samples from the array generated by the script above. By sending the samples one by one to the DAC at a fixed rate, the DAC will generate a sine wave. Varying the rate of sending the samples will change the frequency of the resulting tone:


//--------------------------------------------
// 12 bit DAC test sketch for MCP4921
// Joris Van Looveren
// Based on Bart Venneker's example
// (see http://www.bartvenneker.nl/Arduino/index.php?art=0015 )
//--------------------------------------------
// Connections for the DAC mcp4921
// arduino pin 9 = SS (Slave select, pin 2 on the chip)
// arduino pin 11= Data In (pin 4 on device)
// arduino pin 13= clock (pin 3 on device)
//--------------------------------------------
// We supply a sine wave to the DAC. The sine wave is precalculated, 
// we have 360 values from 0-4000 (+/- 12 bits), that represent
// one full period. 
// The speaker gives off a whistle of about 440 Hz.
//--------------------------------------------

#include <SPI.h>

int index;

static int sineWave[] = {  
  2034, 2069, 2104, 2139, 2174, 2209, 2243, 2278, 2312, 2347, 2381, 2415,
  2449, 2483, 2517, 2551, 2584, 2617, 2651, 2683, 2716, 2749, 2781, 2813,
  2845, 2876, 2907, 2938, 2969, 2999, 3029, 3059, 3089, 3118, 3146, 3175,
  3203, 3231, 3258, 3285, 3311, 3338, 3363, 3389, 3414, 3438, 3462, 3486,
  3509, 3531, 3554, 3575, 3597, 3617, 3638, 3657, 3677, 3695, 3714, 3731,
  3749, 3765, 3781, 3797, 3812, 3826, 3840, 3854, 3866, 3879, 3890, 3901,
  3912, 3922, 3931, 3940, 3948, 3956, 3963, 3969, 3975, 3980, 3985, 3988,
  3992, 3995, 3997, 3998, 3999, 3999, 3999, 3998, 3997, 3995, 3992, 3989, 
  3985, 3980, 3975, 3969, 3963, 3956, 3948, 3940, 3932, 3922, 3912, 3902, 
  3891, 3879, 3867, 3854, 3841, 3827, 3812, 3797, 3782, 3766, 3749, 3732,
  3714, 3696, 3677, 3658, 3638, 3618, 3597, 3576, 3554, 3532, 3509, 3486, 
  3463, 3439, 3414, 3389, 3364, 3338, 3312, 3286, 3259, 3232, 3204, 3176,
  3147, 3119, 3090, 3060, 3030, 3000, 2970, 2939, 2908, 2877, 2846, 2814, 
  2782, 2750, 2717, 2685, 2652, 2619, 2585, 2552, 2518, 2484, 2450, 2416, 
  2382, 2348, 2313, 2279, 2244, 2210, 2175, 2140, 2105, 2070, 2036, 2001, 
  1966, 1931, 1896, 1861, 1826, 1792, 1757, 1722, 1688, 1653, 1619, 1585, 
  1551, 1517, 1483, 1449, 1416, 1383, 1350, 1317, 1284, 1252, 1219, 1187, 
  1155, 1124, 1093, 1062, 1031, 1001,  971,  941,  911,  882,  854,  825,  
   797,  769,  742,  715,  688,  662,  637,  611,  586,  562,  538,  514,
   491,  468,  446,  424,  403,  382,  362,  342,  323,  304,  286,  268,  
   251,  234,  218,  203,  188,  173,  159,  146,  133,  121,  109,   98,
    87,   77,   68,   59,   51,   44,   37,   30,   24,   19,   15,   11,
     7,    4,    2,    1,    0,    0,    0,    1,    2,    4,    7,   10,
    14,   19,   24,   30,   36,   43,   50,   58,   67,   76,   86,   97,
   108,  119,  132,  144,  158,  172,  186,  201,  217,  233,  249,  266,
   284,  302,  321,  340,  360,  380,  401,  422,  444,  466,  489,  512,
   535,  559,  584,  609,  634,  660,  686,  712,  739,  767,  794,  822,
   851,  879,  908,  938,  968,  998, 1028, 1059, 1090, 1121, 1152, 1184,
  1216, 1248, 1281, 1313, 1346, 1379, 1413, 1446, 1480, 1513, 1547, 1581,
  1616, 1650, 1684, 1719, 1753, 1788, 1823, 1858, 1892, 1927, 1962, 1997 };

void setup()
{   
  //pinMode(CS_DAC,OUTPUT);
  DDRB |= B00000010;
  //digitalWrite(CS_DAC,HIGH);
  PORTB |= PORTB | B00000010; 
  SPI.begin();

  index = 0;
}

void loop()
{
  //delayMicroseconds(1); 
  
  int sample = sineWave[index];
  index = (index + 10) % 360;
  Write4921(sample);
}


void Write4921(int value) {
  byte data;

  SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE2));  
  
  //digitalWrite(CS, LOW);
  PORTB &= ~B00000010;

  data = highByte(value);
  data = B00001111 & data;
  data = B01110000 | data;
  SPI.transfer (data); 
  data = lowByte(value);
  SPI.transfer (data); 

  //digitalWrite(CS, HIGH);
  PORTB |= B00000010;

  SPI.endTransaction();
}

2. Hearing the tone

The output of the DAC is not powerful enough to drive a speaker. In order to be able to hear anything, the DAC signal needs to be amplified. As a first test, the DAC is coupled to a known-quantity amplifier: a LM386. Both the DAC and the amplifier are put on a piece of strip-board, and connected to the Arduino’s 5V supply and the SPI signals. The schematic was taken from a forum post on the Arduino forum.

Schematic:

DAC/amp setup schematic

On strip-board:

DAC + amp on stripboard
DAC + amp on stripboard 2

In the repository, the code and the schematic can be found under the tag “Output_DAC+amp_works”.

3. Upgrade to ARM

The Arduino is limited in the sample rate it can achieve. In order to be able to play sound at 44.1KHz, a more powerful controller is needed. From previous projects, I was familiar with TI’s Tiva C development board. The Arduino code was transferred to the Tiva C board at first, and later to the CC3200 board.

The challenging part here is to use the Tiva’s (and CC3200’s) hardware SPI interface. In the repository, the tag “16_kHz_sample” represents this step.

4. Actual samples

With hardware part of the sound output working, the next step was to play actual samples from a sound file. To keep this test as simple as possible, a similar approach was used: the samples from a sound file were put into a C array, scaled from 16 bit samples at 16KHz (later 22KHz), to values between 0 and 4096. The code then sends the samples to the DAC at a fixed rate of 22050 samples per second.


#!/usr/bin/python
import struct

max_line_length = 50

min_val = 0
max_val = 0

# Determine min and max values in file
with open('moby_45sec_22kHz.pcm', 'rb') as infile:
    short = infile.read(2)
    while short != b"":
        val = struct.unpack('<h', short)[0]
        if (val < min_val):
            min_val = val
        if (val > max_val):
            max_val = val
        short = infile.read(2)

# Scaling factor to rescale min_val .. max_val -> 0 .. 4096,
# with the middle shifted from 0 to 2048
scaling_factor = 2048.0 / max(-min_val,max_val)

# Reread the file, now writing the output file at the same time
cur_line_length = 0
sample_count = 0
with open('sample_arr.h', 'w') as outfile:
    outfile.write('uint16_t samples[] = {\n     ');
    with open('moby_45sec_22kHz.pcm', 'rb') as infile:
        short = infile.read(2)
        while short != b"":
            # we have a next value, so write a ',' or a newline
            if sample_count > 0:
                if cur_line_length < max_line_length-5:
                    outfile.write(', ')
                else:
                    outfile.write(',\n     ')
                    cur_line_length = 0

            val = struct.unpack('<h', short)[0]
            str = "%d" % (2048 + (val * scaling_factor))
            outfile.write(str)
            cur_line_length += len(str)

            sample_count += 1
            short = infile.read(2)
    outfile.write('\n};\n')

print "Wrote %d samples." % sample_count    

5. MP3 decoding

With raw audio output working, the next step was then to add MP3 decoding to the mix. The search for a small but decent MP3 decoding library settled quite quickly on the Helix MP3 decoder (https://www.helixcommunity.org/projects/datatype/mp3dec). The source code can be found in many places on the Internet.

Initially I took the same approach as for WAV (raw samples) playing: take an MP3 file, convert it (partially) to a C array, and include it as static data in the executable. This allows testing without the overhead of opening files etc.

In the repository, under the tag “mp3_decoding_works”, the main.c file shows how to decode several MP3 blocks one after another.

After getting the decoder to work for single blocks of data, the next step was to massage the mechanism so it can be run iteratively to decode continous data. This is reflected in the repository under the tag “mp3_continuous_decoding_works”.

The Helix library was adapted slightly. The “straight” implementation writes the audio samples into an output buffer. This requires allocation of a fairly large buffer up front. The library was adapted to instead call a callback function to store decoded samples. This gives some more flexibility in the application, for example to add the samples to a ring buffer instead of a simple linear buffer.

6. Screen

The LCD screen is a known quantity: the old Nokia 5110 cell phone screen. We have a few of the Adafruit breakout boards, since we’ve used it before in the portable game console. In that project, we already built a driver library to use the LCD with the TI Tiva board; this translates easily to the CC3200.

We don’t use the hardware SPI interface with the LCD, but instead “bit-bang” the protocol on a set of general-purpose I/O pins (GPIO). The main reason to do this is that the LCD uses a “data/command” line in addition to the normal SPI communication lines, which would have to be controlled outside of the SPI communication.

An additional benefit of not using the hardware SPI module for the LCD is that we’re sure that we will not interfere with audio playback if we have to control the screen while audio is being played.

In the project repository, the files display.h and display.c contain the high-level interface to the LCD. The file letters.h contains the character definitions used by the display driver; the file hal.c contains the low-level communication routines that implement the “faux-SPI” interface.


With feasibility of the core parts proven, we’re ready to move on to flesh out all components into the real thing.

Parts