Arduino Serial Port Audio Streaming

1. Introduction
In the last few years, the Arduino has probably been one of the most written about and discussed microprocessor systems around. However, there don’t seem to be many projects around which explore the audio streaming capabilities of the Arduino, so I thought I’d take a shot at one.

Briefly, audio signals from a micophone are amplified and input to an A/D input on the Arduino. The Arduino firmware samples and digitizes an audio signal at a 20kHz rate, and transfers the data over a serial port to a PC at 230400 baud. On the PC side, a Python script reads the serial data, plays it back and displays it on an oscilloscope-like screen.

2. The Circuit


As shown above, an Electret Microphone, biased by R1 is input to U1, an LM386 which is used as a preamplifier. The audio signal is input to op-amp U2 which is biased to 1/2 Vcc by R3 and R4. Note that an audio signal can be fed directly to U2 via switch SW1. U2 has a gain of about 10, with a high frequency cut-off of about 5kHz due to R5 and C3. The signal is further filtered and decoupled by R6 andC4. R7 and R8 bias the signal to 1/2 the Arduino Vcc of 2.5V where it is input to the first A/D channel A0.

3. The Arduino Firmware
The Arduino firmware was written in the C language developed on Atmel Studio 7. Its fairly straightforward, after the serial port, timers and A/D are configured, a timer initiates ADC conversions at a 20 KHz rate. At the end of each conversion, an interrupt service routine transfers the data to the UART, and transmits the result over the serial port at 230400 baud. It could probably be adapted to run under the Arduino IDE but it would end up looking a lot like a regular C program.

1. Serial Port Initialization
The serial port initialization is listed below:

#define F_CPU 16000000UL
#define USART_BAUDRATE 230400
#define UBRR_VALUE ((F_CPU/4/USART_BAUDRATE)-1)/2

// Set the baud rate divider to 8 to double the
// transfer rate
UCSR0A |= (1<<U2X0);
// Set baud rate based on UBBR_VALUE
UBRR0H = (uint8_t)(UBRR_VALUE>>8);
UBRR0L = (uint8_t)UBRR_VALUE;
// Set frame format to 8 data bits, no parity, 1 stop bit
UCSR0C |= (1<<UCSZ01)|(1<<UCSZ00);
// Enable transmission and reception
UCSR0B |= (1<<RXEN0)|(1<<TXEN0);

There is a macro to set the UART baud rate in the utils library of the Atmel compiler but its not very accurate above a rate of 19200 kb/sec. To set higher baud rates I’ve used a formula found in the Adruino IDE tools which sets the baud rate register based on the processor speed. Note that the baud rate divider has to be set to 8 instead of it default value of 16.

2. Initialize Debug Pins
Pins 2 and 6 of I/O Port D are being used as debug pins. These correspond to pins 2 and 6 of the Arduino IOL connector.

// Initialize debug ports
void InitPort(void)
{
 // Set PD6 and PD2 as output
 DDRD |= (1<<PD2)|(1<<PD6);
}

3. Initialize and Start Timer 0
Timer0 controls the timing for the A/D converter. It is conigured to initiate a conversion every 50usec by counting up to a value set in the ouptut comapare register OCRA according to the following formula:

OCRA = ((period/8)*F_CPU) – 1
= (50usec/8)*16000000) – 1
= 99

Note that the timer prescaler must be set to 8.
The code is shown below:

// Initialize timer0
void InitTimer0(void)
{
 // Set Initial Timer value
 TCNT0=0;
 // Place TOP timer value to Output compare register
 OCR0A=99;
 // Set CTC mode
 // and make toggle PD6/OC0A pin on compare match
 TCCR0A |=(1<<COM0A0)|(1<<WGM01);
}

// Start timer0 with prescaler 8
void StartTimer0(void)
{
 // Set prescaler 8 and start timer
 TCCR0B |=(1<<CS01);
}

4. Initialize and Start the A/D Converter

The ADC initialization code is shown below:

void InitADC()
{
 // Select Vref=AVcc
 // and set left adjust result
 ADMUX |= (1<<REFS0)|(1<<ADLAR);
 // Set prescaler to 32
 // Enable auto-triggering
 // Enable ADC interrupt
 // and enable ADC
 ADCSRA |= (1<<ADPS2)|(1<<ADPS0)|(1<<ADATE)|(1<<ADIE)|(1<<ADEN);
 // Set ADC trigger source - Timer0 compare match A
 ADCSRB |= (1<<ADTS1)|(1<<ADTS0);
}

The ADC data in the ATmega328 have a 10 bit value, of which we need the upper 8 bits. Setting ADLAR in the ADMUX register shifts the upper 8 bits of the result into the higher byte of the ADC data register, where it will be copied into the UART.

The ADC is configured to run in auto-triggered mode by setting ADATE to 1, this means that it will be triggered by a selected source, in this case the ouput of the compare register from Timer0. Setting ADIE to 1 sets an interrupt when the conversion is complete.

The following code selects the ADC channel:

void SetADCChannel(uint8_t ADCchannel)
{
 // Select ADC channel with safety mask
 ADMUX = (ADMUX & 0xF0) | (ADCchannel & 0x0F);
}

As shown below, setting bit 6 (ADSC) in the ADC control register starts the first conversion:

void StartADC(void)
{
 ADCSRA |= (1<<ADSC);
}

5. ADC Conversion Interrupt Service Routine

The ADC ISR is fairly straightforward. At the end of a conversion, the Timer0 compare flag is cleared, pin PD2 is toggled for troubleshooting purposes, and the data is transferred to the serial port.

ISR(ADC_vect)
{
 // Clear timer compare match flag
 TIFR0=(1<<OCF0A);
 // Toggle pin PD2 to track the end of ADC conversion
 PIND = (1<<PD2);
 // Wait while previous byte is completed
 while(!(UCSR0A&(1<<UDRE0))){};
 // Transmit data
 UDR0 = ADCH;
}

6. The Main Program Loop

As shown below, the main program consists of enabling the serial port, ADC, timer and ISR. Then sitting back and let the timer and interrupt do their thing:

int main(void)
{
 // Initialize USART0
 USART0Init();
 // Initialize ports
 InitPort();
 // Initialize ADC
 InitADC();
 // Select ADC channel
 SetADCChannel(0);
 // Initialize timer0
 InitTimer0();
 // Start timer0
 StartTimer0();
 // Start conversion
 StartADC();
 // Enable global interrupts
 sei();

 while(1)
 { 
 }
}

The complete Arduino source code can be found here: arduino_main.zip

4. The Python Script

The Python script pyScope.py. Uses pySerial to receive the serial data. A pyAudio callback function plays back the received audio, and a TkInter based GUI displays the data in an oscilloscope screen.

The comple script can be found here: pyScope.zip

1. Data Acquisition

The DataAq class is responsible for acquiring audio data from the serial port and sending it to the audio callback function and the TkInter GUI. The class initializes by looking for a serial port with an Arduino attached. If none is found, a message is set and a con_err flag is set.

The actual serial port data acquisition is run as a background thread, and the data is passed to other modules by way of queues, a plot_q and audio_q. If no connection error exists, data is read as a 2048 byte array and put onto the plot_q. If a connection error exists, data is generated by a random number generator and passed to the queue.

If no connection error exists, the serial data is also passed as a 2048 byte array to the audio_q for the audio callback functon.

The audio callback function is defined in the GUI class. When a new audio data buffer is required, the callback function reads data from audio_q and plays it back.

The data acquisition thead can be paused when the program is stopped, and stopped when the script is terminated.

2. TkInter GUI

As shown in the screen-shot above, the GUI implements a basic oscilloscope-like display of the received audio data with some timing, triggering, and audio playback functions.

  • The Start/Stop button gets things rolling by enabling or pausing the data acquisition thread and starting or stopping the audio stream.
  • The Trig On/Off button ensures that the display is started at a fixed trigger level (set at 128) on a positive slope.
  • The Audio On/Off button starts and stops the audio stream and sets flags for the GUI and data acquisition thread to enable audio playback.
  • The Sweep Time list box determines the time duration of the display width. This value also determines the number of samples for a given sweep time based on the sampling rate, and hence the number of samples per pixel for a given screen width.
  • The Exit button terminates the data acquisition thread, and audio stream, then terminates the GUI.
  • The display screen is a TkInter Canvas object 350 pixels wide by 300 pixels high. It is initialized with a horizontal and vertical grid. Note that drawing on a canvas seems to require a horizontal and vertical offset of three pixels apparently due to the width of the border.
  • The updateGUI() function is a thread that runs concurrently with the TkInter mainloop() function. It restarts every 1 msec to check the data queue for content. If there is a block of data in the queue and the trigger conditions are met, the function scales the data to the screen height and width, and updates the display screen.

The command self.plotter.after(1,self.updateGUI) ensures that the function will restart every 1 msec.

3. __main__  Function
The ‘main’ function instantiates the the Gui class, then instantiates and starts the data acquisition thread. The updateGUI() needs to be run once to start its 1 msec cycle. Finally the TkInter mainloop() function is called to service the Gui controls.

5. Conclusion

Using the Arduino as a standalone audio streaming device works surprisingly well considering the eight bit resolution and comparatively low sampling rate. As is, it can even be pressed into duty as a low frequency oscilloscope. A better GUI could be designed for a scope, and it should even be possible to implement an audio storage feature.

As usual these things are never finished.