🔧 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
noteNamesandfrequenciestogether by their array positions. If the system encounters the character'C'at index0, it checks index0of the frequency array to fetch262Hz.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
scorearray holds the actual notes for "Happy Birthday". The phrase "Happy-Birth-Day-To-You" is stored literally asG G A G c B.Rhythm Array: The
beatsarray defines note lengths. A value of1represents a quick note,2represents a medium note, and4represents a long held note.Length Auto-Calculation: The
sizeof(score) - 1expression 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 = 200sets 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 startingG 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 subsequentdelay(duration)command is required to halt the processor and let the note finish playing before the next loop cycle begins.
// 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, themap()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 thebeatUnitvariable (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.