Playing Audio with Arduino

Playing audio with Arduino opens up endless possibilities, from automated alerts to interactive robotics projects. In this guide, I’ll show you how to generate audio using an Arduino and a buzzer (or even a small speaker). We’ll start with a simple circuit, then explore how adding a potentiometer lets you adjust the pitch and delay for more control over the sound.

This following circuit shows an Arduino Uno connected to an audio output subsystem, designed to generate audible tones while filtering high-frequency digital noise and providing analog volume control.

Playing Audio with Arduino

🔧 Components Involved

  • Arduino Uno → Provides digital signal (square wave) from pin 2.

  • Resistor R1 (1kΩ) → Works with capacitor C1 to form a LPF.

  • Capacitor C1 (100nF) → Works with R1 to form a filter/timing network.

  • Variable Resistor RV1 (10kΩ) → Adjustable resistance, controls volume.

  • Sounder LS1 (buzzer/speaker) → Produces audible sound when driven.

Here is how the individual sections work together:

1. The Signal Source (Arduino Pin 3)

  • The yellow wire connects Digital Pin 3 of the Arduino Uno to the input of the audio circuit.

  • Pin 3 is a hardware PWM-capable pin. In the context of your tone-playing code, it outputs a digital square wave oscillating between 0V and 5V at specific musical frequencies.

2. The Low-Pass RC Filter ($R_1$ and $C_1$)

  • Components: Resistor R1 (1kΩ) and Capacitor C1 (100nF) form a classic low-pass resistance-capacitance (RC) filter.

  • Function: A digital square wave from the Arduino contains harsh, high-frequency harmonics that sound metallic or raspy. This filter rounds off the sharp edges of the square wave, smoothing it into a cleaner wave. This protects the speaker from excessive high-frequency stress and makes the audio tone sound softer and warmer.

3. Volume Control (RV1)

  • Component: RV1 is a 10kΩ potentiometer configured as a variable voltage divider.

  • Function: The top pin receives the smoothed audio signal from the RC filter, the bottom pin is tied to Ground, and the middle pin (the wiper) sweeps between them.

  • Moving the wiper alters the amplitude of the AC audio voltage fed to the speaker, acting as a manual volume knob.

4. The Audio Output (LS1)

  • Component: LS1 is a Sounder (piezo element or high-impedance speaker transducer).

  • Connection: The positive terminal connects to the volume wiper of RV1, while the negative terminal hooks into the common ground line. This ground line completes the loop by running straight back into the Arduino's GND pin.

  • When the fluctuating voltage from the wiper passes through LS1, it flexes the internal element back and forth, turning the electrical pulses into physical sound waves.

Program Code

The program code/sketch below acts like a tiny MIDI player. It works by creating a lookup dictionary that maps standard letter characters to physical audio frequencies, reading a "sheet music" string, and executing the hardware commands to play the notes.


// Pin for the speaker
const int speakerPin = 3;

// Note names and frequencies (including low and high variations)
char noteNames[] =     {'C', 'D', 'E', 'F', 'G', 'A', 'B', 'c', 'd'};
unsigned int frequencies[] = {262, 294, 330, 349, 392, 440, 494, 523, 587};
const byte noteCount = sizeof(noteNames);

// Complete melody with correct octaves (lowercase = higher octave)
char score[] = "GGAGcBGGAGdcGGgECBAFFECDC";

// Exact rhythm: 1 = quarter note, 2 = half note, 4 = dotted half note
byte beats[] = {1, 1, 2, 2, 2, 4,   1, 1, 2, 2, 2, 4,   1, 1, 2, 2, 2, 2, 2,   1, 1, 2, 2, 2, 4};

const byte scoreLen = sizeof(score) - 1; // -1 ignores the hidden null terminator

void setup() {
  pinMode(speakerPin, OUTPUT);
}

void loop() {
  int beatUnit = 200; // Base speed of the song (lower = faster)
  
  for (int i = 0; i < scoreLen; i++) {
    int duration = beats[i] * beatUnit;
    playNote(score[i], duration);
    
    // CRITICAL: This short pause prevents the notes from bleeding together
    delay(duration * 0.20); 
  }
  
  delay(4000); // Wait 4 seconds before singing again
}

void playNote(char note, int duration) {
  bool matched = false;
  for (int i = 0; i < noteCount; i++) {
    if (noteNames[i] == note) {
      tone(speakerPin, frequencies[i], duration);
      matched = true;
      break;
    }
  }
  
  // Keep the Arduino busy for the exact duration of the note
  delay(duration);
}
  

Technical Breakdown

1. The Lookup Dictionary

  • Parallel Arrays: The script links noteNames and frequencies together by their array positions. If the system encounters the character 'C' at index 0, it checks index 0 of the frequency array to fetch 262 Hz.

  • Octave Handling: The character matching is case-sensitive. Capital letters represent standard notes (e.g., 'C' is 262 Hz), while lowercase letters are used to signify the higher octave (e.g., 'c' is 523 Hz, exactly double the frequency).

2. The Sheet Music

  • Melody String: The score array holds the actual notes for "Happy Birthday". The phrase "Happy-Birth-Day-To-You" is stored literally as G G A G c B.

  • Rhythm Array: The beats array defines note lengths. A value of 1 represents a quick note, 2 represents a medium note, and 4 represents a long held note.

  • Length Auto-Calculation: The sizeof(score) - 1 expression automatically detects the song length while ignoring the hidden null terminator byte (\0) that C adds to the end of strings.

3. The Playback Loop

  • Tempo Selection: The variable beatUnit = 200 sets the base timing in milliseconds. Decreasing this number increases the playback speed.

  • Note Separation: The formula delay(duration * 0.20) introduces a brief silence after each note. This prevents back-to-back identical notes (like the starting G G) from bleeding together into a single continuous buzz.

4. Audio Generation

  • Hardware Timers: The tone() function toggles physical digital Pin 3 at the designated frequency using internal hardware timers. It outputs a square wave to oscillate the speaker.

  • Asynchronous Execution: Because tone() runs in the background using hardware interrupts, the subsequent delay(duration) command is required to halt the processor and let the note finish playing before the next loop cycle begins.

We can modify the circuit a bit to include pitch and delay control. The following circuit shows how.

Playing Audio with Arduino with pitch and delay control

The code for this schematic is below:
  // Pin Configurations
const int speakerPin = 3;    // Out to Low-Pass Filter and Sounder
const int pitchPotPin = A0;  // RV2 wiper for frequency modulation
const int tempoPotPin = A1;  // RV3 wiper for playback speed control

// Note dictionary with baseline frequencies
char noteNames[] =     {'C', 'D', 'E', 'F', 'G', 'A', 'B', 'c', 'd'};
unsigned int baseFrequencies[] = {262, 294, 330, 349, 392, 440, 494, 523, 587};
const byte noteCount = sizeof(noteNames);

// Sheet music data for "Happy Birthday"
char score[] = "GGAGcBGGAGdcGGgECBAFFECDC";
byte beats[] = {1, 1, 2, 2, 2, 4,   1, 1, 2, 2, 2, 4,   1, 1, 2, 2, 2, 2, 2,   1, 1, 2, 2, 2, 4};
const byte scoreLen = sizeof(score) - 1; // Excludes string null terminator

void setup() {
  pinMode(speakerPin, OUTPUT);
  // Analog pins A0 and A1 configure automatically for analogRead()
}

void loop() {
  // Read RV3 (A1) to dynamically establish the song's tempo
  // Maps 0-5V (0-1023 ADC values) to a base beat unit of 50ms (fast) to 600ms (slow)
  int beatUnit = map(analogRead(tempoPotPin), 0, 1023, 50, 600);
  
  // Read RV2 (A0) to grab the raw position for the real-time pitch offset
  int pitchShiftRaw = analogRead(pitchPotPin);

  for (int i = 0; i < scoreLen; i++) {
    // Calculate the physical duration of the current note
    int duration = beats[i] * beatUnit;
    
    // Process and play the note with the calculated frequency scaling
    playNoteWithShift(score[i], duration, pitchShiftRaw);
    
    // Distinct note-separation pause (staccato effect)
    delay(duration * 0.20); 
  }
  
  delay(4000); // 4-second delay before restarting the melody
}

void playNoteWithShift(char note, int duration, int shiftRaw) {
  for (int i = 0; i < noteCount; i++) {
    if (noteNames[i] == note) {
      // Map raw analog data to scale the base frequency 
      // Fully left shifts down 1 octave (/2), fully right shifts up 1 octave (*2)
      unsigned int shiftedFrequency = map(shiftRaw, 0, 1023, baseFrequencies[i] / 2, baseFrequencies[i] * 2);
      
      // Fire internal hardware timers to output square wave on Pin 3
      tone(speakerPin, shiftedFrequency, duration);
      break;
    }
  }
  // Hold the program execution while the background timer plays the note
  delay(duration);
}
  

How the Hardware Interacts with this Code

Pitch Control (RV2 $\rightarrow$ A0)

  • Hardware Action: Rotating RV2 alters the DC voltage moving into Analog Pin 0 between 0V and 5V.

  • Code Execution: The internal ADC converts this voltage into a digital scale from 0 to 1023. Inside playNoteWithShift, the map() function uses this integer to recalculate the baseline note arrays on the fly. Sweeping the pot fully left cuts the current musical pitch in half (lowering it by an octave), while sweeping it fully right doubles the frequency (shifting it up an octave).

Tempo / Delay Control (RV3 $\rightarrow$ A1)

  • Hardware Action: Rotating RV3 modifies the voltage input entering Analog Pin 1.

  • Code Execution: Before starting the playback loop, analogRead(A1) captures the current knob placement. The code maps this value directly to the beatUnit variable (ranging between 50 and 600 milliseconds). Adjusting this knob immediately alters the speed of the song at the next loop restart, providing responsive real-time tempo control.


Related tutorials

Post a Comment

Previous Post Next Post