Introduction: Stereo Audio With Arduino

About: I post updates on twitter and instagram: @amandaghassaei

Recently I've been posting a lot of projects that use an 8 bit resistor ladderdigital to analog converter (DAC) and an Arduino to make sound. (see the Arduino vocal effects box, the Arduino drum sampler, and my audio output tutorial). The technique I've been using to make these DACs is very simple, it requires only a handful of 10k and 20k resistors wired together into a network. But the convenience comes with a price, as these DACs end up a little noisier than I would like at times. So I decided to buy a specialized IC that will be compatible with all the code I've already written for the resistor ladder DACs, but uses highly matched resistors to reduce noise. When I looked on Digikey for such a DAC, I found the TLC7528, a dual output 8 bit DAC IC. The dual output capability of the chip interested me a lot; while it is easy to set this chip up with one permanent output, it also gives you the option of toggling between two isolated output pins, making it fairly straightforward to set up a 2 channel audio output with a relatively small amount of additional effort/hardware setup/Arduino data pins.

In this instructable I'll show you how to use the TLC7528 with the Arduino to output stereo audio. Stereo audio means 2 independent channels of audio. Stereo audio is especially fun when sent to headphones because you can achieve some interesting auditory effects since each ear is hearing its own independent channel of sound, some ideas include:

"3D audio" spatial effects- by adjusting the filtering, amplitude, and phase of two channels of audio you can simulate the experience of sound directionality, making a sound source seem to originate from a precise location in the space around you, here's a great example
binaural beats- by sending two sine waves of similar -but unequal- frequencies to headphones (one to each ear), you will hear a pulsating beatnote that is thought to induce relaxation and other meditative effects. Here's an example.
panning- change the relative amplitude of a sound source in each channel of the stereo mix. This effect is simple, but can be really cool sounding, a great example is in the bridge of Led Zeppelin's Whole Lotta Love (listen to it with headphones!)

Parts List:

(x1) TLC7528 Digikey 296-1871-5-ND

(1x) Arduino Uno Amazon

(1x) usb cable Amazon

(1x) breadboard (this one comes with jumper wires) Amazon

(1x) jumper wires Amazon

Other Materials:
oscillosope

Step 1: 8 Bit DACs and Serial Vs Parallel

The TLC7528 is a type digital to analog converter (DAC). It takes digital data (numbers between 0 and 255) and outputs a voltage between 0 and whatever voltage you supply the chip with. The output voltage of the DAC can be calculated according to the following equation:

output voltage from 8 bit DAC = (supply voltage) * (digital input data) / 255

In this Instructable, I'll be powering the DAC from the Arduino's built in 5V supply, so the equation above can be simplified to:

output voltage from 8 bit DAC = 5V * (digital input data) / 255

From this equation, we can see that the TLC7528 would output 5V if it receives a value of 255, 0V if it receives a value of 0, 2.5V if it receives a value of 127, and so on. You may be wondering where the 255 came from, this is a result of the TLC7528 being an 8 bit DAC. 8 bit means that the binary numbers we can send to the DAC must have no more than 8 digits in them. In binary, numbers that are represented with 8 digits (or less) range in value from 0 to 255 (as opposed to the regular decimal numeral system where and 8 digit numbers range from 0 to 99999999). So there are 256 possible values (0-255) that the 8 bit DAC can receive. This can be calculated quickly from the equation below:

2^8 = 256 possible values

If we were using a 10 bit DAC, then it could receive 2^10 = 1024 different values, ranging from 0-1023. This means a 10 bit DAC has a higher resolution than an 8 bit DAC. Despite this, I've found that 8 bit DAC's are generally much more useful than 10 bit DACs because data is easier to store and output from the Arduino in 8 bit form than in 10 bit form. For example, the data type byte in the Arduino language is for storing 8 bit numbers. If you wanted to store a 10 bit number, you would have to use an int data type, but int data types can store up to 16 bit numbers, so you would be wasting 6 bits of memory. Additionally, the pins of the Arduino are grouped together in clusters of 8 or less. On the Uno, the only full group of 8 are digital pins 0-7, this group is called PORTD. When writing Arduino code you can easily output 8 bits of data by setting the states of digital pins 0-7 all at once. In the code this is done by sending an 8 bit number to PORTD. For example:

PORTD = 255;

sets digital pins 0-7 HIGH, it is equivalent to the following:

digitalWrite(0,HIGH);
digitalWrite(1,HIGH);
digitalWrite(2,HIGH);
digitalWrite(3,HIGH);
digitalWrite(4,HIGH);
digitalWrite(5,HIGH);
digitalWrite(6,HIGH);
digitalWrite(7,HIGH);

but using the PORTD command sets all the pins simultaneously and is much faster. The following command would set digital pins 0-7 LOW:

PORTD = 0;

you can also use the PORTD command to set some of the pins high and others low. For example:

PORTD = 137;
137 in binary is 10001001, so sending 137 to PORTD will set pin 7 HIGH (because the first digit of the binary number is a "1"), pins 6-4 LOW (because the next 3 digits are "0"), pin 3 HIGH, pins 2 and 1 LOW, and pin 0 HIGH. You can read more about how this works on the Arduino website. You can even send binary numbers to PORTD, for example:

PORTD = B10001001;
is equivalent to PORTD = 137;

Finally, I'll talk about how we get this data into the TLC7528. The TLC7528 is called a parallel DAC. This means that all the data we send to the DAC is sent in parallel. 8 bit parallel DACs have eight data connections between the Arduino and DAC that send all 8 bits of data at the same time. The opposite of parallel is serial, in serial setups you use fewer data connections (usually three), but send only one bit over at a time. So, in order to transmit an 8 bit number via a serial connection you have to send eight 1 bit packages, one after the other, while in parallel setups you can send all 8 bits at the same time. This means that serial connections require faster data transfer than parallel connections. If you are not worried about using 8 digital pins of the Arduino, a parallel 8 bit DAC is a good option because it requires less clock speed and is simpler to code.

Step 2: TLC7528 Overview

The pin diagram shown above comes directly from the datasheet of the TLC7528 DAC. As I described in the last step, the TLC7528 has 8 parallel data inputs, labelled DB0-DB7; these pins will connect directly to digital pins 0-7 of the Arduino. The TLC7528 is an interesting chip because it actually has two outputs on it (called DACA and DACB), which are both connected to the same 8 digital input pins. You can select which output to want to use by setting pin 6 HIGH or LOW (LOW outputs to DACA and high outputs to DACB). Pins 15 and 16 are used to control the outputs of the DAC as well, I'll explain more in later steps.

Fig 2 shows a functional block diagram of the chip. Again, notice the data inputs, logic controls, and two separate outputs of the DAC. There are many ways to set up this DAC depending on what you are trying to do with it, you can read more about these on the TLC7528 datasheet. Fig 3 shows how to set up the DAC in voltage-mode operation. In this setup, the analog output of the DAC will be pins REFA and REFB, and the pins labeled OUTA and OUTB will be connected to a fixed input voltage (I used the 5V supply voltage). In the next step I'll show you how I set this up on a breadboard.

Step 3: Mono Audio Output With 8 Bit DAC and 44.1kHz Sampling Rate

The schematic for the DAC setup is shown in fig 2. AGND and DGND (pins 1 and 5) connect to Arduino ground. VDD (pin 17), OUTA (pin 2), OUTB (pin 20). RFBA (pin 3), and RFBB (pin 19) connect to Arduino 5V. WR (pin 16) connects to digital pin 10, CS (pin 15) connects to digital pin 9, and DACA/DACB (pin 6) connects to digital pin 8. DB0-DB7 (pins 14-7) connect to digital pins 0-7. The outputs from the DAC are pins 4 (for DACA) and 18 (for DACB).

In the following piece code I use a timer interrupt to send data to the DAC at a rate of about 44.1kHz (standard audio sampling rate). Interrupts are routines that are executed at specifically timed intervals. While the Arduino is running commands in the main loop() function it pauses briefly to execute the contents of ISR(TIMER1_COMPA_vect){} at a rate of 44.1kHz. Once the commands inside the function are executed, the Arduino resumes what it was doing in the loop() function. This way we can easily add code to the loop function (check sensors, turn on leds, etc) and not have to worry about the timing of the audio output. The following lines set up the interrupt:

cli();//stop interrupts

//set timer1 interrupt at ~44.1kHz
TCCR1A = 0;// set entire TCCR1A register to 0
TCCR1B = 0;// same for TCCR1B
TCNT1 = 0;//initialize counter value to 0
// set compare match register for 1hz increments
OCR1A = 361;// = (16*10^6) / (44100*1) - 1 (must be <65536)
// turn on CTC mode
TCCR1B |= (1 << WGM12);
// Set CS10 bit for 1 prescaler
TCCR1B |= (1 << CS10);
// enable timer compare interrupt
TIMSK1 |= (1 << OCIE1A);

sei();//enable interrupts

A full explanation of these lines is given in my Arduino Timer interrupt tutorial. Inside the interrupt routine we find the following lines:

PORTD = saw;//send saw out to the DAC through digital pins 0-7
saw++;//increment saw value by one
if (saw==256){//reset saw if it reaches 256 (keeps output within 0-255 always)
saw=0;
}

So each time the interrupt routine executes it sends the value of the variable "saw" to the DAC. Then the variable saw is increased by one for the next time the interrupt routine executes. If saw is >255 it is reset to zero. Essentially the Arduino is sending the numbers 0-255 to the DAC and then resetting back to 0 once it reaches 255. This will output a saw wave from the DAC (fig 7).

I noted in the comments of the code that the interrupt routine is not executing at exactly 44.1kHz, but as close as I could get. The actual sampling rate is 44.199kHz (which is actually slightly better than 44.1). This was due to some limitations in the setup of timer interrupts. We'll use this number to calculate some info about the DAC output:

duration of each sample = 1/sampling rate
duration of each sample = 1/44199Hz = 22.6us

Fig 8 shows a zoomed in view of the DAC saw output on an oscilloscope. You can see the individual 22.6us steps of the wave, just as calculated. The period of the wave (the length of one complete saw cycle) is:

period = duration of each sample * samples per cycle
period = 22.6us * 256 = 5.8ms

and the frequency:

frequency = 1/period
frequency = 1/0.0058s = 172Hz

these can be seen in fig 6.
<pre>//mono saw out with 44.1kHz sampling rate
//by Amanda Ghassaei
//Nov 2012
//https://www.instructables.com/id/Stereo-Audio-with-Arduino/

/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
*/

//control pins of TLC7528
#define outputSelector 8
#define CS 9
#define WR 10

byte saw = 0;//value of saw wave

void setup(){
  
  for (byte i=0;i<8;i++){
    pinMode(i, OUTPUT);//set digital pins 0-7 as outputs
  }
  
  pinMode(outputSelector,OUTPUT);
  pinMode(CS,OUTPUT);
  pinMode(WR,OUTPUT);

  cli();//stop interrupts

  //set timer1 interrupt at ~44.1kHz
  TCCR1A = 0;// set entire TCCR1A register to 0
  TCCR1B = 0;// same for TCCR1B
  TCNT1  = 0;//initialize counter value to 0
  // set compare match register for 1hz increments
  OCR1A = 361;// = (16*10^6) / (44100*1) - 1 (must be <65536)
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // Set CS10 bit for 1 prescaler
  TCCR1B |= (1 << CS10);  
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);
  
  sei();//enable interrupts
  
  //select output (LOW for DACA and HIGH for DACB)
  digitalWrite(outputSelector,LOW);
  
  //set CS and WR low to let incoming data from digital pins 0-7 go to DAC output
  digitalWrite(CS,LOW);
  digitalWrite(WR,LOW);
  
}

ISR(TIMER1_COMPA_vect){//timer1 interrupt ~44.1kHz to send audio data (it is really 44.199kHz)
  PORTD = saw;//send saw out to the DAC through digital pins 0-7
  saw++;//increment saw value by one
  if (saw==256){//reset saw if it reaches 256 (keeps output within 0-255 always)
    saw=0;
  }
}


void loop(){
  //do other stuff here
}

The code below outputs a sine wave using the same interrupt I set up above. Arduino has a built in sine function, but it is too slow to execute at 44.1kHz, so I stored an array of sine values to pull from during each interrupt. I ran a simple Python script (below) to generate 100 values of 127+127*sin(2*3.14*t/100):

import math
for x in range(0, 100):
print str(int(127+127*math.sin(2*math.pi*x*0.01)),)+str(",")
,

I stored these numbers in an array called "sine" and then put the following line in the interrupt routine:

PORTD = sine[index];//send a value stored in the array sine out to the DAC through digital pins 0-7
index++;//increment index by one
if (index==100){//reset index if it reaches 100
index=0;
}

these lines send a value from the array "sine", specified by the value of the variable "index", to PORTD. Then "index" is increased by one for the next cycle. If index > 99 it gets reset to zero. The output from this code is shown in fig 9.

Since I used the same interrupt setup as the saw code, the duration of each sample is the same: 22.6us (fig 10). Since there are only 100 samples per cycle for this sine (vs 255 samples per cycle for the saw) the period is:

period = duration of each sample * samples per cycle
period = 22.6us * 100 = 2.3ms

frequency = 1/period
frequency = 1/0.0023s = 442Hz

these can be seen in fig 9
<pre>//mono sine out with 44.1kHz sampling rate
//by Amanda Ghassaei
//Nov 2012
//https://www.instructables.com/id/Stereo-Audio-with-Arduino/

/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
*/

//control pins of TLC7528
#define outputSelector 8
#define CS 9
#define WR 10

//100 values of a single cycle of sine centered around 127 with an amplitude of 127
byte sine[] = {127, 134, 142, 150, 158, 166, 173, 181, 188, 195, 201, 207, 213, 219, 224, 229, 234, 238, 241, 245, 247, 250, 251, 252, 253, 254, 253, 252, 251, 250, 247, 245, 241, 238, 234, 229, 224, 219, 213, 207, 201, 195, 188, 181, 173, 166, 158, 150, 142, 134, 127, 119, 111, 103, 95, 87, 80, 72, 65, 58, 52, 46, 40, 34, 29, 24, 19, 15, 12, 8, 6, 3, 2, 1, 0, 0, 0, 1, 2, 3, 6, 8, 12, 15, 19, 24, 29, 34, 40, 46, 52, 58, 65, 72, 80, 87, 95, 103, 111, 119,};

byte index = 0;//index variable for extracting data from array "sine"

void setup(){
  
  for (byte i=0;i<8;i++){
    pinMode(i, OUTPUT);//set digital pins 0-7 as outputs
  }
  
  pinMode(outputSelector,OUTPUT);
  pinMode(CS,OUTPUT);
  pinMode(WR,OUTPUT);

  cli();//stop interrupts

  //set timer1 interrupt at ~44.1kHz
  TCCR1A = 0;// set entire TCCR1A register to 0
  TCCR1B = 0;// same for TCCR1B
  TCNT1  = 0;//initialize counter value to 0
  // set compare match register for 1hz increments
  OCR1A = 361;// = (16*10^6) / (44100*1) - 1 (must be <65536)
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // Set CS10 bit for 1 prescaler
  TCCR1B |= (1 << CS10);  
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);
  
  sei();//enable interrupts
  
  //select output (LOW for DACA and HIGH for DACB)
  digitalWrite(outputSelector,LOW);
  
  //set CS and WR low to let incoming data from digital pins 0-7 go to DAC output
  digitalWrite(CS,LOW);
  digitalWrite(WR,LOW);
  
}

ISR(TIMER1_COMPA_vect){//timer1 interrupt ~44.1kHz to send audio data (it is really 44.199kHz)
  PORTD = sine[index];//send a value stored in the array sine out to the DAC through digital pins 0-7
  index++;//increment index by one
  if (index==100){//reset index if it reaches 100
    index=0;
  }
}


void loop(){
  //do other stuff here
}

Step 4: Stereo Audio Output With 8 Bit DAC and 44.1kHz Sampling Rate

In this code I am sending a sine wave out DACA and a saw wave out DACB at the same time and at a rate of 44.1kHz. This is stereo audio, two separate channels of audio. To get this to work, I combined elements sine and saw mono code from the last step and used the WR and DACA/DACB pins to toggle between the two DAC outputs.

I set up an interrupt like in the last step, but this time I set it up at a rate of 2*44.1 = 88.2kHz. Then each time the interrupt executed, I alternated between sending out something to DACA and DACB, so each received a sample during every other interrupt. This makes the sampling rate on both of the outputs 44.1kHz. The contents of the interrupt routine are copied below:

digitalWrite(WR,HIGH);//hold outputs- so new DAC data does not get sent out until we are ready
if (channel){
PORTD = sine[index];//send sine to digital pins 0-7
digitalWrite(outputSelector,LOW);//select DACA
index++;//increment index value by one
if (index==100){//reset index if it reaches 100
index=0;
}
}
else{
PORTD = saw;//send saw to digital pins 0-7
digitalWrite(outputSelector,HIGH);//select DACB
saw++;//increment saw value by one
if (saw==255){//reset saw if it reaches 256 (keeps output within 0-255 always)
saw=0;
}
}
digitalWrite(WR,LOW);//enable output again
channel ^=1;//toggle channel

When the interrupts starts, the Arduino sets the WR pin HIGH, this temporarily holds the DAC outputs at their current voltages and allows us to send data into the DAC without changing the current selected DAC output. The variable "channel" toggles between values of 0 and 1 each time the interrupt executes, alternating the sine and saw output. When "channel" = 1, a value from the array "sine" is set to the DAC via PORTD. The next line sets the outputSelector pin (DACA/DACB pin) LOW, which causes DACA to be selected. Then WR is set LOW, causing the new sine value to output via DACA. In the next interrupt routine, a similar series of events causes a saw value to output from DACB.
<pre>//stereo audio out, sampling rate <=44.1kHz
//by Amanda Ghassaei
//Nov 2012
//https://www.instructables.com/id/Stereo-Audio-with-Arduino/

/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
*/

//control pins of TLC7528
#define outputSelector 8
#define CS 9
#define WR 10

byte saw = 0;//value of saw wave

//100 values of sine, centered at 127, amplitud of 127
byte sine[] = {127, 134, 142, 150, 158, 166, 173, 181, 188, 195, 201, 207, 213, 219, 224, 229, 234, 238, 241, 245, 247, 250, 251, 252, 253, 254, 253, 252, 251, 250, 247, 245, 241, 238, 234, 229, 224, 219, 213, 207, 201, 195, 188, 181, 173, 166, 158, 150, 142, 134, 127, 119, 111, 103, 95, 87, 80, 72, 65, 58, 52, 46, 40, 34, 29, 24, 19, 15, 12, 8, 6, 3, 2, 1, 0, 0, 0, 1, 2, 3, 6, 8, 12, 15, 19, 24, 29, 34, 40, 46, 52, 58, 65, 72, 80, 87, 95, 103, 111, 119,};
byte index = 0;//index to retreive values fom array "sine"

boolean channel = 0;//state of 0 sends to one channel, 1 sends to other channel

void setup(){
  
  for (byte i=0;i<8;i++){
    pinMode(i, OUTPUT);//set digital pins 0-7 as outputs
  }
  
  pinMode(outputSelector,OUTPUT);
  pinMode(CS,OUTPUT);
  pinMode(WR,OUTPUT);

  cli();//stop interrupts

  //set timer1 interrupt at ~88.2kHz (2*44.1kHz)
  TCCR1A = 0;// set entire TCCR1A register to 0
  TCCR1B = 0;// same for TCCR1B
  TCNT1  = 0;//initialize counter value to 0
  // set compare match register for 1hz increments
  OCR1A = 180;// = (16*10^6) / (88200*1) - 1 (must be <65536)
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // Set CS10 bit for 1 prescaler
  TCCR1B |= (1 << CS10);  
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);
  
  sei();//enable interrupts

  //set CS pin low
  digitalWrite(CS,LOW);
  
}

ISR(TIMER1_COMPA_vect){//timer1 interrupt ~88.2kHz to send audio data (actually at 88.398kHz)
  digitalWrite(WR,HIGH);//hold outputs- so new DAC data does not get sent out until we are ready
  if (channel){
    PORTD = sine[index];//send sine to digital pins 0-7
    digitalWrite(outputSelector,LOW);//select DACA
    index++;//increment index value by one
    if (index==100){//reset index if it reaches 100
      index=0;
    }
  }
  else{
    PORTD = saw;//send saw to digital pins 0-7
    digitalWrite(outputSelector,HIGH);//select DACB
    saw++;//increment saw value by one
    if (saw==255){//reset saw if it reaches 256 (keeps output within 0-255 always)
      saw=0;
    }
  }
  digitalWrite(WR,LOW);//enable output again
  channel ^=1;//toggle channel
}


void loop(){
  //do other stuff here
}

As in the last step, my sampling rate was not exactly 88.2kHz, it was actually 88.398kHz (slightly better than 88.2), so I'll use that number in the following calculations:

duration of each sample = 2 * 1/sampling rate
duration of each sample = 2 * 1/88398Hz = 22.6us

as in the last step the period of the sine and saw are as follows:

saw period = 22.6us * 256 = 5.8ms
sine period = 22.6us * 100 = 2.3ms

but if you look at figs 2 and 3 you will see that the sample duration and period of the output waves is much longer. This is because the code in the interrupt routine is inefficient and is taking longer than 22.6us to execute. To fix this I had to replace the Arduino library command "digitalWrite" with much more efficient direct pin manipulation commands in the code below. You can read more about how they work here, you can also read the comments I've put in the code below. Figs 4 and 5 show the outputs from this optimized code, you can see that the period and sample durations are what we expect from the calculations.
<pre>//stereo audio out with 44.1kHz sampling rate
//by Amanda Ghassaei
//Nov 2012
//https://www.instructables.com/id/Stereo-Audio-with-Arduino/

/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
*/


//control pins of TLC7528
#define outputSelector 8
#define CS 9
#define WR 10

byte saw = 0;//value of saw wave

//100 values of sine, centered at 127, amplitude of 127
byte sine[] = {127, 134, 142, 150, 158, 166, 173, 181, 188, 195, 201, 207, 213, 219, 224, 229, 234, 238, 241, 245, 247, 250, 251, 252, 253, 254, 253, 252, 251, 250, 247, 245, 241, 238, 234, 229, 224, 219, 213, 207, 201, 195, 188, 181, 173, 166, 158, 150, 142, 134, 127, 119, 111, 103, 95, 87, 80, 72, 65, 58, 52, 46, 40, 34, 29, 24, 19, 15, 12, 8, 6, 3, 2, 1, 0, 0, 0, 1, 2, 3, 6, 8, 12, 15, 19, 24, 29, 34, 40, 46, 52, 58, 65, 72, 80, 87, 95, 103, 111, 119,};
byte index = 0;//index to retreive values fom array "sine"

boolean channel = 0;//state of 0 sends to one channel, 1 sends to other channel

void setup(){
  
  for (byte i=0;i<8;i++){
    pinMode(i, OUTPUT);//set digital pins 0-7 as outputs
  }
  
  pinMode(outputSelector,OUTPUT);
  pinMode(CS,OUTPUT);
  pinMode(WR,OUTPUT);

  cli();//stop interrupts

  //set timer1 interrupt at ~88.2kHz (2*44.1kHz)
  TCCR1A = 0;// set entire TCCR1A register to 0
  TCCR1B = 0;// same for TCCR1B
  TCNT1  = 0;//initialize counter value to 0
  // set compare match register for 1hz increments
  OCR1A = 180;// = (16*10^6) / (88200*1) - 1 (must be <65536)
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // Set CS10 bit for 1 prescaler
  TCCR1B |= (1 << CS10);  
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);
  
  sei();//enable interrupts

  //set CS pin low
  digitalWrite(CS,LOW);
  
}

ISR(TIMER1_COMPA_vect){//timer1 interrupt ~88.2kHz to send audio data (actually at 88.398kHz)
  PORTB |= B00000100;//digitalWrite(WR,HIGH);//hold outputs- so new DAC data does not get sent out until we are ready
  if (channel){
    PORTD = sine[index];//send sine to digital pins 0-7
    PORTB &= B11111110;//digitalWrite(outputSelector,LOW);//select DACA
    index++;//increment index value by one
    if (index==100){//reset index if it reaches 100
      index=0;
    }
  }
  else{
    PORTD = saw;//send saw to digital pins 0-7
    PORTB |= B00000001;//digitalWrite(outputSelector,HIGH);//select DACB
    saw++;//increment saw value by one
    if (saw==255){//reset saw if it reaches 256 (keeps output within 0-255 always)
      saw=0;
    }
  }
  PORTB &= B11111011;//digitalWrite(WR,LOW);//enable output again
  channel ^=1;//toggle channel
}


void loop(){
  //do other stuff here
}

I'll also note here, that since the CS pin is held LOW for the duration of this code (setting it HIGH will disable writing new data to either output), you could free up an extra Arduino pin by attaching CS to ground permanently and deleting the instances of CS in the Arduino code.

Step 5: Amplifier, Additional Circuitry, and Tips

If you want to send the output of this dual channel DAC to speakers you will need use some additional circuitry. Steps 3-8 of my Arduino Audio Output tutorial describe how to buffer, low pass filter, amplify, and DC offset an audio signal to send it to speakers. You will need to build a separate copy of these circuits for each channel of audio.

I've also written a comprehensive list of potential issues with using 8 bit parallel DACs and their solutions in Step 10 of my Arduino Audio Output tutorial. The problems I cover include:

-using serial communication (this is usually handled by digital pins 0 and 1)
-changing the pin connections to the DAC (if you need to use PWM pins or make the DAC compatible with a shield/some of the code)
-running out of digital inputs/outputs
-adapting this code to Mega or other boards

Step 6: Binaural Beats With Arduino

Binaural beats are an interesting effect of sending two sine waves of slightly different frequencies to headphones (one sine wave to each ear). Listen to this for an example (you must listen with headphones). When listening to this example, try listening to just one headphone by itself and then the other. You will find that the sounds coming out of each channel are pure sine waves of slightly different frequencies, and when you listen to them together you perceive a pulsating effect. Many people believe that certain combinations of frequencies help with focus, meditation, sleep, and other brain activities. I don't know enough about binaural beats to comment on this, but I am interested in the fact that this pulsating effect exists in the first place.

If you've ever tried to tune an instrument, you may be familiar with the concept of beatnotes (also called dissonance notes). When you hear two frequencies that are very close to each other you start to hear a pulsating tremolo effect (called a "beat"). This effect is easily explained by interference between the two similar waves. In this picture you can see two waves of very similar frequencies on the bottom, and their sum on the top, notice how the top signal varies in amplitude over time, this is the beat note. The frequency of the beat is equal to the difference between the two frequencies. For example if you play a 300hz and 305hz signal at the same time, you will hear a 5hz beat. As you tune the 305hz signal closer to the 300hz signal, you will hear the beat slow down and eventually disappear when the two frequencies are equal, here is an example. The interesting thing about binaural beats is that the two signals are never physically mixed together like they are for the beat notes I've just described, in binaural beats each frequency is sent separately to one ear. All the signal mixing to produce something like a beatnote happens inside our brains, seemingly by the interference of the electrical/chemical signals coming from each ear.

To set this up I increased the resolution of the stored sine function to 1000 samples by running the following Python script and saving the array of values in my Arduino code:

import math
for x in range(0, 1000):
print str(int(127+127*math.sin(2*math.pi*x*0.001)),)+str(",")
,

I sent the sine waves out each channel similarly to step 4, but instead I incremented the index variables for each sine wave by different amounts for each interrupt cycle. For instance:

index1 += 10;
will increment the index1 variable by ten each time a new value of sine is sent to DACA. The frequency of the resulting wave is calculated as follows:

frequency = [ (interrupt frequency)*(index incrementation) ] / [ (samples in sine array)*(number of channels) ]
frequency = [ 88398*10) ] / [ 1000*2 ] = 442Hz

by incrementing inex1 and index2 at slightly different rate, you can send different frequency sine waves (440 and 480hz in this example) out each channel do the DAC.
<pre>//binaural beats stereo audio with 44.1kHz sampling rate
//by Amanda Ghassaei
//Nov 2012
//https://www.instructables.com/id/Stereo-Audio-with-Arduino/

/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
*/

#include <avr/pgmspace.h>

//control pins of TLC7528
#define outputSelector 8
#define CS 9
#define WR 10

//1000 values of sine, centered at 127, amplitude of 127
byte sine[] PROGMEM = {127, 127, 128, 129, 130, 130, 131, 132, 133, 134, 134, 135, 136, 137, 138, 138, 139, 140, 141, 142, 142, 143, 144, 145, 146, 146, 147, 148, 149, 150, 150, 151, 152, 153, 153, 154, 155, 156, 157, 157, 158, 159, 160, 160, 161, 162, 163, 163, 164, 165, 166, 167, 167, 168, 169, 170, 170, 171, 172, 173, 173, 174, 175, 175, 176, 177, 178, 178, 179, 180, 181, 181, 182, 183, 183, 184, 185, 186, 186, 187, 188, 188, 189, 190, 190, 191, 192, 193, 193, 194, 195, 195, 196, 197, 197, 198, 199, 199, 200, 201, 201, 202, 202, 203, 204, 204, 205, 206, 206, 207, 207, 208, 209, 209, 210, 210, 211, 212, 212, 213, 213, 214, 215, 215, 216, 216, 217, 217, 218, 219, 219, 220, 220, 221, 221, 222, 222, 223, 223, 224, 224, 225, 225, 226, 226, 227, 227, 228, 228, 229, 229, 230, 230, 231, 231, 232, 232, 232, 233, 233, 234, 234, 235, 235, 235, 236, 236, 237, 237, 237, 238, 238, 239, 239, 239, 240, 240, 240, 241, 241, 241, 242, 242, 242, 243, 243, 243, 244, 244, 244, 245, 245, 245, 245, 246, 246, 246, 247, 247, 247, 247, 248, 248, 248, 248, 248, 249, 249, 249, 249, 250, 250, 250, 250, 250, 250, 251, 251, 251, 251, 251, 251, 252, 252, 252, 252, 252, 252, 252, 252, 252, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 254, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 252, 252, 252, 252, 252, 252, 252, 252, 252, 251, 251, 251, 251, 251, 251, 250, 250, 250, 250, 250, 250, 249, 249, 249, 249, 248, 248, 248, 248, 248, 247, 247, 247, 247, 246, 246, 246, 245, 245, 245, 245, 244, 244, 244, 243, 243, 243, 242, 242, 242, 241, 241, 241, 240, 240, 240, 239, 239, 239, 238, 238, 237, 237, 237, 236, 236, 235, 235, 235, 234, 234, 233, 233, 232, 232, 232, 231, 231, 230, 230, 229, 229, 228, 228, 227, 227, 226, 226, 225, 225, 224, 224, 223, 223, 222, 222, 221, 221, 220, 220, 219, 219, 218, 217, 217, 216, 216, 215, 215, 214, 213, 213, 212, 212, 211, 210, 210, 209, 209, 208, 207, 207, 206, 206, 205, 204, 204, 203, 202, 202, 201, 201, 200, 199, 199, 198, 197, 197, 196, 195, 195, 194, 193, 193, 192, 191, 190, 190, 189, 188, 188, 187, 186, 186, 185, 184, 183, 183, 182, 181, 181, 180, 179, 178, 178, 177, 176, 175, 175, 174, 173, 173, 172, 171, 170, 170, 169, 168, 167, 167, 166, 165, 164, 163, 163, 162, 161, 160, 160, 159, 158, 157, 157, 156, 155, 154, 153, 153, 152, 151, 150, 150, 149, 148, 147, 146, 146, 145, 144, 143, 142, 142, 141, 140, 139, 138, 138, 137, 136, 135, 134, 134, 133, 132, 131, 130, 130, 129, 128, 127, 127, 126, 125, 124, 123, 123, 122, 121, 120, 119, 119, 118, 117, 116, 115, 115, 114, 113, 112, 111, 111, 110, 109, 108, 107, 107, 106, 105, 104, 103, 103, 102, 101, 100, 100, 99, 98, 97, 96, 96, 95, 94, 93, 93, 92, 91, 90, 90, 89, 88, 87, 86, 86, 85, 84, 83, 83, 82, 81, 80, 80, 79, 78, 78, 77, 76, 75, 75, 74, 73, 72, 72, 71, 70, 70, 69, 68, 67, 67, 66, 65, 65, 64, 63, 63, 62, 61, 60, 60, 59, 58, 58, 57, 56, 56, 55, 54, 54, 53, 52, 52, 51, 51, 50, 49, 49, 48, 47, 47, 46, 46, 45, 44, 44, 43, 43, 42, 41, 41, 40, 40, 39, 38, 38, 37, 37, 36, 36, 35, 34, 34, 33, 33, 32, 32, 31, 31, 30, 30, 29, 29, 28, 28, 27, 27, 26, 26, 25, 25, 24, 24, 23, 23, 22, 22, 21, 21, 21, 20, 20, 19, 19, 18, 18, 18, 17, 17, 16, 16, 16, 15, 15, 14, 14, 14, 13, 13, 13, 12, 12, 12, 11, 11, 11, 10, 10, 10, 9, 9, 9, 8, 8, 8, 8, 7, 7, 7, 6, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 16, 16, 16, 17, 17, 18, 18, 18, 19, 19, 20, 20, 21, 21, 21, 22, 22, 23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 28, 28, 29, 29, 30, 30, 31, 31, 32, 32, 33, 33, 34, 34, 35, 36, 36, 37, 37, 38, 38, 39, 40, 40, 41, 41, 42, 43, 43, 44, 44, 45, 46, 46, 47, 47, 48, 49, 49, 50, 51, 51, 52, 52, 53, 54, 54, 55, 56, 56, 57, 58, 58, 59, 60, 60, 61, 62, 63, 63, 64, 65, 65, 66, 67, 67, 68, 69, 70, 70, 71, 72, 72, 73, 74, 75, 75, 76, 77, 78, 78, 79, 80, 80, 81, 82, 83, 83, 84, 85, 86, 86, 87, 88, 89, 90, 90, 91, 92, 93, 93, 94, 95, 96, 96, 97, 98, 99, 100, 100, 101, 102, 103, 103, 104, 105, 106, 107, 107, 108, 109, 110, 111, 111, 112, 113, 114, 115, 115, 116, 117, 118, 119, 119, 120, 121, 122, 123, 123, 124, 125, 126,};
int index1 = 0;//index to retreive values fom array "sine"
int index2 = 0;//index to retreive values fom array "sine"

boolean channel = 0;//state of 0 sends to one channel, 1 sends to other channel

void setup(){
  
  for (byte i=0;i<8;i++){
    pinMode(i, OUTPUT);//set digital pins 0-7 as outputs
  }
  
  pinMode(outputSelector,OUTPUT);
  pinMode(CS,OUTPUT);
  pinMode(WR,OUTPUT);

  cli();//stop interrupts

  //set timer1 interrupt at ~88.2kHz (2*44.1kHz)
  TCCR1A = 0;// set entire TCCR1A register to 0
  TCCR1B = 0;// same for TCCR1B
  TCNT1  = 0;//initialize counter value to 0
  // set compare match register for 1hz increments
  OCR1A = 180;// = (16*10^6) / (88200*1) - 1 (must be <65536)
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // Set CS10 bit for 1 prescaler
  TCCR1B |= (1 << CS10);  
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);
  
  sei();//enable interrupts

  //set CS pin low
  digitalWrite(CS,LOW);
  
}

ISR(TIMER1_COMPA_vect){//timer1 interrupt ~88.2kHz to send audio data (actually at 88.398kHz)
  PORTB |= B00000100;//digitalWrite(WR,HIGH);//hold outputs- so new DAC data does not get sent out until we are ready
  if (channel){
    PORTD = pgm_read_byte_near(sine + index1);//send sine to digital pins 0-7
    PORTB &= B11111110;//digitalWrite(outputSelector,LOW);//select DACA
    index1 += 10;//increment index value, increment by 10 =~ 440hz
    if (index1>=1000){//reset index if it reaches 1000
      index1=0;
    }
  }
  else{
    PORTD = pgm_read_byte_near(sine + index2);//send sine to digital pins 0-7
    PORTB |= B00000001;//digitalWrite(outputSelector,HIGH);//select DACB
    index2 += 11;//increment index value, increment by 11 =~ 480hz
    if (index2>=1000){//reset index if it reaches 1000
      index2=0;
    }
  }
  PORTB &= B11111011;//digitalWrite(WR,LOW);//enable output again
  channel ^=1;//toggle channel
}


void loop(){
  //do other stuff here
}

Step 7: Simple Panning Circuit With Arduino

In this piece of code I used a potentiometer to control the pan between two channels of audio.  First I wired up the middle lead of a potentiometer to A0 and the outside leads to 5V and ground.  (I used a 100kOhm potentiometer, but anything between 1k and 1M will work fine too).  Then I uploaded the following code:
This code outputs 440hz sine waves out both stereo outputs.  When the potentiometer is turned all the way to one side, one of the outputs will be at maximum amplitude and the other will flat-line at 0 (fig 2).  As the potentiometer is turned in the other direction, the amplitude of one channel will go down and the other will go up until the channel which was previously flat-lining is at full amplitude and the other channel is at 0 (fig 3-6).

This code is fairly straightforward, but it uses a few tricks to maximize efficiency that I'll explain a little further here.  As in step 4, I've replaced all the digitalWrite() commands with PORTB &= and PORTB |= commands.  I did this purely for speed reasons (when you're working with 88kHz interrupts, you need to work quickly), and the comments of each of these lines gives their equivalent Arduino library command.  for example the line:

PORTB &= B11111011;

is equivalent to:

digitalWrite(WR,LOW);
where WR = digital pin 10

more info about how these commands work can be found on the Arduino website.  The function checkPan() checks the state of the pan potentiometer.  Since it is not critical to measure this pot at a high frequency, I only called the function in the main loop() instead of the interrupt.  The contents of checkPan()  are repeated below:

pan = analogRead(A0);
  pan = pan >> 3;//convert from 10 bit to 7 bit (0-127)
  panA = pan;
  panB = 127-pan;


First the variable pan is set to the output from analogRead(A0).  The function analogRead() will always return a number between 0 and 1023- a 10 bit number.  I didn't think such high resolution was necessary for what I was doing, so I reduced the resolution of this number down to 7 bit in the second line to make it a bit more manageable.  Now instead of being a number between 0 and 1023, pan has been scaled down to fit between 0 and 127.  I did this with a simple bit shift, you could also do this with some division, or the map function, they all have the same end result (but the bit shift is the fastest way to do this).  Now that I have pan, a value between 0 and 127, I did some simple subtraction to calculate the amplitudes to send to DACA and DACB and assigned these values to panA and panB. 

In the interrupt routine I replaced the simple sine output from step 4:

PORTD = sine[index2];

with an output that has been scaled:

PORTD = (sine[index2]*panB)>>7;

this line is functionally equivalent to the following:

PORTD = (sine[index2]*panB)/127;

but since it uses a bit shift instead of division, it is much more efficient.  This is very important in high frequency interrupts, in fact, the line above will slow down the interrupt so much that it will not be able to execute fully before it is time for a new interrupt to start.  Using this bit shift trick is the only way to get the code to work correctly.
DIY Audio

Participated in the
DIY Audio