From Digital Bits to Sound Waves: Arduino Nano Synth Project
Table of Contents
Description
Even though I went to an elementary school focused on music, I was in the only non-musical class. It wasn’t so bad… I didn’t have much interest in music anyway.
I regret it a little, especially after I saw Ben Eater’s new video “Computer Noises”, where he used his breadboard 6502 computer to generate a PWM signal to play different notes. I was vaguely familiar with sound generated by code, but I had never done it myself or researched the topic. Around the same time, I received a few Arduino Nano clones from Aliexpress and decided I wanted to try it!
As expected, I wanted to do more than just call the ‘tone()’ function. I fell into a rabbit hole of learning what digital and analog sound are, the math behind it, and how to read notes. In the end, I’m happy with the result and my new knowledge about sound and music.
Consulting AI
Sometimes consulting AI to learn about a topic of interest can be annoying because it yaps too much. Google Gemini solved this with a feature called “Guided Learning”, where it gives small, incremental explanations and then asks you questions. This was my starting point to learn about music. What pitch, notes, octaves, and rests are. From there, I was ready to watch a video series on YT, how to read sheet music and put all of it together.
I was convinced that I could build this the next day… Oh boy, I was so wrong. Understanding how to create sound from digital input took me so long, this wasn’t clicking at all. After the high of understanding something new, I was brought back down by not understanding anything again… A classic sine wave of learning.
Digital to Analog
Another stop on my journey was discovering that I needed to convert 1s and 0s into a voltage range (DAC) to make the speaker play something. From building an 8-bit computer on a breadboard, I had tons of resistors, transistors, capacitors, and ICs left over. The most common way to build a DAC is to use an R-2R ladder. All I needed were resistors, and I had a big bag of 1kΩ resistors, so I used them. By changing the input values, in our case, 1s and 0s we get different voltage levels on the output. How precise this output is depends on how large our ladder is. In my opinion, the sweet spot for this project is 8-bit.
12-bit or more is overkill for learning, and 4-bit just doesn’t feel right. I had two options to connect this ladder to the Arduino Nano. Use 8 pins and control each one, or dig into my IC collection and use a shift register. Option 2 won. I used a 74HC595 8-bit shift register.
I wrote simple code to shift data into the 595 connected to the R-2R. GPT recommended that I loop over pre-calculated values of a sine wave, so I did that. The result on the oscilloscope looked fine to my amateur eyes. I felt like I was unstoppable once again and this time, I could finish it in half a day.
Reality check again
Let’s connect a small speaker and hear how this sine wave sounds. Nothing happened. Did I break something? Maybe the speaker doesn’t work… I leaned back in my chair, disappointed with myself again. After thinking it over with AI, yeah, I needed something called an amplifier. This was the biggest stop on my journey. I’ll confess. Understanding the concept of what an amplifier is and how it works took me a few days.
I was too deep into this! I wasn’t going to buy amplifier ICs from Aliexpress. I will build one myself! Luckily for me (and for this project), I was able to build an amp using an transistor, a few resistors, and some capacitors. A Class A amplifier circuit uses only one transistor, so that’s the one for me.
A few videos, articles, calculations, and days later, I think, I got somewhere! The speaker is working, something is playing… it’s the sound of a sine wave, I hope.
But by now, you know the drill… after putting it all together and trying to play “music” based on notes, reality came knocking on my door again.
The timing of the notes was off, the notes sounded off, the signal on the oscilloscope looked off, back to the AI chat drawing board.
In the end, the distortion was too much and the volume was too low or after tinkering with capacitors, the volume was too high. So I tried using a Class AB amplifier with an NPN and a PNP transistor. Building it was much easier after all the trial and error with the single transistor setup, and right from the start the volume was perfect with minimal distortion. To make it even better, I could add two diodes, but I don’t have any, so I’m happy with the result.
Analog to Digital back to Analog
Ok, let’s try to learn how we get these 1s and 0s from an analog signal in the first place… Alright, so something called sample rate and bit depth exists… I know that! I’ve seen it when setting my audio settings on my PC! Ok, it’s not that hard. Sample rate is just the number of samples taken over a period of time, and bit depth is the resolution, how precise the values are for each sample.
A few rewrites, some tuning, and more reading later, I finally had something usable. I am happy with this… Sounds fine, volume level is good to and minimal distortion too.
Chiisana Boukensha - Konosuba! ED
Code
#include <Arduino.h>
#include <SPI.h>
#include <avr/pgmspace.h>
#define SHIFT_RCLK 3 // Latch Pin (PD3)
// SPI MOSI = Pin 11
// SPI SCK = Pin 13
// Calculate Timer Value: (16,000,000 / SampleRate) - 1
// 16000000 / 22050 = ~725.6 -> 725
// #define SAMPLE_RATE 22050.0
// #define TIMER_COMPARE 725
#define SAMPLE_RATE 44000.0
#define TIMER_COMPARE 363
// --- MUSIC DEFINITIONS ---
#define WHOLE 4.0
#define HALF 2.0
#define QUARTER 1.0
#define EIGHTH 0.5
#define SIXTEENTH 0.25
#define TRIPLET (1.0/3.0)
// --- GLOBAL VARIABLES (Volatile is required for Interrupts) ---
volatile unsigned int phaseAccumulator = 0; // Current position in sine wave
volatile unsigned int phaseIncrement = 0; // Speed (Pitch) - "0" means silence
volatile uint8_t idx = 0;
// https://ppelikan.github.io/drlut/
const uint8_t sine256[256] PROGMEM = {
127,130,133,136,139,143,146,149,152,155,158,161,164,
167,170,173,176,178,181,184,187,190,192,195,198,200,
203,205,208,210,212,215,217,219,221,223,225,227,229,
231,233,234,236,238,239,240,242,243,244,245,247,248,
249,249,250,251,252,252,253,253,253,254,254,254,254,
254,254,254,253,253,253,252,252,251,250,249,249,248,
247,245,244,243,242,240,239,238,236,234,233,231,229,
227,225,223,221,219,217,215,212,210,208,205,203,200,
198,195,192,190,187,184,181,178,176,173,170,167,164,
161,158,155,152,149,146,143,139,136,133,130,127,124,
121,118,115,111,108,105,102, 99, 96, 93, 90, 87, 84,
81, 78, 76, 73, 70, 67, 64, 62, 59, 56, 54, 51, 49,
46, 44, 42, 39, 37, 35, 33, 31, 29, 27, 25, 23, 21,
20, 18, 16, 15, 14, 12, 11, 10, 9, 7, 6, 5, 5,
4, 3, 2, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 2, 2, 3, 4, 5, 5, 6, 7, 9,
10, 11, 12, 14, 15, 16, 18, 20, 21, 23, 25, 27, 29,
31, 33, 35, 37, 39, 42, 44, 46, 49, 51, 54, 56, 59,
62, 64, 67, 70, 73, 76, 78, 81, 84, 87, 90, 93, 96,
99,102,105,108,111,115,118,121,124
};
enum Notes {
NOTE_REST,
NOTE_C1, NOTE_CS1, NOTE_D1, NOTE_DS1, NOTE_E1, NOTE_F1,
NOTE_FS1, NOTE_G1, NOTE_GS1, NOTE_A1, NOTE_AS1, NOTE_B1,
NOTE_C2, NOTE_CS2, NOTE_D2, NOTE_DS2, NOTE_E2, NOTE_F2,
NOTE_FS2, NOTE_G2, NOTE_GS2, NOTE_A2, NOTE_AS2, NOTE_B2,
NOTE_C3, NOTE_CS3, NOTE_D3, NOTE_DS3, NOTE_E3, NOTE_F3,
NOTE_FS3, NOTE_G3, NOTE_GS3, NOTE_A3, NOTE_AS3, NOTE_B3,
NOTE_C4, NOTE_CS4, NOTE_D4, NOTE_DS4, NOTE_E4, NOTE_F4,
NOTE_FS4, NOTE_G4, NOTE_GS4, NOTE_A4, NOTE_AS4, NOTE_B4,
NOTE_C5, NOTE_CS5, NOTE_D5, NOTE_DS5, NOTE_E5, NOTE_F5,
NOTE_FS5, NOTE_G5, NOTE_GS5, NOTE_A5, NOTE_AS5, NOTE_B5,
NOTE_C6, NOTE_CS6, NOTE_D6, NOTE_DS6, NOTE_E6, NOTE_F6,
NOTE_FS6, NOTE_G6, NOTE_GS6, NOTE_A6, NOTE_AS6, NOTE_B6,
NOTE_C7, NOTE_CS7, NOTE_D7, NOTE_DS7, NOTE_E7, NOTE_F7,
NOTE_FS7, NOTE_G7, NOTE_GS7, NOTE_A7, NOTE_AS7, NOTE_B7,
NOTE_C8, NOTE_CS8, NOTE_D8, NOTE_DS8, NOTE_E8, NOTE_F8,
NOTE_FS8, NOTE_G8, NOTE_GS8, NOTE_A8, NOTE_AS8, NOTE_B8
};
const int noteFreq[] PROGMEM = {
0, // REST
33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62, // Octave 1
65, 69, 73, 78, 82, 87, 93, 98, 104, 110, 117, 123, // Octave 2
131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247, // Octave 3
262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, // Octave 4
523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, // Octave 5
1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, // Octave 6
2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951, // Octave 7
4186, 4435, 4699, 4978, 5274, 5588, 5920, 6272, 6645, 7040, 7459, 7902 // Octave 8
};
struct NoteEvent {
uint8_t note;
float beats;
};
// --- KONOSUBA THEME --- BPM = 79
const NoteEvent song[] PROGMEM = {
{NOTE_C5, QUARTER}, {NOTE_C5, EIGHTH}, {NOTE_D5, EIGHTH},
{NOTE_E5, QUARTER}, {NOTE_E5, EIGHTH}, {NOTE_G5, EIGHTH},
{NOTE_A5, SIXTEENTH}, {NOTE_C6, SIXTEENTH}, {NOTE_A5, SIXTEENTH}, {NOTE_G5, SIXTEENTH},
{NOTE_E5, QUARTER}, {NOTE_E5, EIGHTH}, {NOTE_D5, EIGHTH},
{NOTE_C5, QUARTER}, {NOTE_A5, QUARTER},
{NOTE_G5, SIXTEENTH}, {NOTE_E5, SIXTEENTH}, {NOTE_D5, SIXTEENTH}, {NOTE_G4, SIXTEENTH},
{NOTE_C5, QUARTER}, {NOTE_C5, EIGHTH}, {NOTE_D5, EIGHTH},
{NOTE_C5, HALF}
};
// --- KOROBEINIKI (Tetris Theme) --- BPM = 160
// const NoteEvent song[] PROGMEM = {
// // -- SECTION A --
// {NOTE_E5, QUARTER}, {NOTE_B4, EIGHTH}, {NOTE_C5, EIGHTH}, {NOTE_D5, QUARTER},
// {NOTE_C5, EIGHTH}, {NOTE_B4, EIGHTH},
// {NOTE_A4, QUARTER}, {NOTE_A4, EIGHTH}, {NOTE_C5, EIGHTH}, {NOTE_E5, QUARTER},
// {NOTE_D5, EIGHTH}, {NOTE_C5, EIGHTH},
// {NOTE_B4, QUARTER + EIGHTH}, {NOTE_C5, EIGHTH}, {NOTE_D5, QUARTER}, {NOTE_E5, QUARTER},
// {NOTE_C5, QUARTER}, {NOTE_A4, QUARTER}, {NOTE_A4, QUARTER}, {NOTE_REST, QUARTER},
// // -- SECTION B (High Part) --
// {NOTE_D5, QUARTER + EIGHTH}, {NOTE_FS5, EIGHTH}, {NOTE_A5, QUARTER},
// {NOTE_G5, EIGHTH}, {NOTE_FS5, EIGHTH},
// {NOTE_E5, QUARTER + EIGHTH}, {NOTE_C5, EIGHTH}, {NOTE_E5, QUARTER},
// {NOTE_D5, EIGHTH}, {NOTE_C5, EIGHTH},
// {NOTE_B4, QUARTER}, {NOTE_B4, EIGHTH}, {NOTE_C5, EIGHTH},
// {NOTE_D5, QUARTER}, {NOTE_E5, QUARTER},
// {NOTE_C5, QUARTER}, {NOTE_A4, QUARTER}, {NOTE_A4, QUARTER}, {NOTE_REST, QUARTER}
// };
// --- IMPERIAL MARCH (Star Wars) --- BPM = 108
// const NoteEvent song[] PROGMEM = {
// // Main Riff
// {NOTE_G4, QUARTER}, {NOTE_G4, QUARTER}, {NOTE_G4, QUARTER},
// {NOTE_DS4, EIGHTH + SIXTEENTH}, {NOTE_AS4, SIXTEENTH}, {NOTE_G4, QUARTER},
// {NOTE_DS4, EIGHTH + SIXTEENTH}, {NOTE_AS4, SIXTEENTH}, {NOTE_G4, HALF},
// // Second Phrase (Higher)
// {NOTE_D5, QUARTER}, {NOTE_D5, QUARTER}, {NOTE_D5, QUARTER},
// {NOTE_DS5, EIGHTH + SIXTEENTH}, {NOTE_AS4, SIXTEENTH}, {NOTE_FS4, QUARTER},
// {NOTE_DS4, EIGHTH + SIXTEENTH}, {NOTE_AS4, SIXTEENTH}, {NOTE_G4, HALF}
// };
// --- FUR ELISE (Beethoven) --- BPM = 65
// const NoteEvent song[] PROGMEM = {
// // -- Main Theme --
// {NOTE_E5, SIXTEENTH}, {NOTE_DS5, SIXTEENTH},
// {NOTE_E5, SIXTEENTH}, {NOTE_DS5, SIXTEENTH},
// {NOTE_E5, SIXTEENTH}, {NOTE_B4, SIXTEENTH},
// {NOTE_D5, SIXTEENTH}, {NOTE_C5, SIXTEENTH},
// {NOTE_A4, QUARTER}, {NOTE_REST, SIXTEENTH},
// // -- Arpeggio 1 (Low to High) --
// {NOTE_C4, SIXTEENTH}, {NOTE_E4, SIXTEENTH}, {NOTE_A4, SIXTEENTH},
// {NOTE_B4, QUARTER}, {NOTE_REST, SIXTEENTH},
// // -- Arpeggio 2 --
// {NOTE_E4, SIXTEENTH}, {NOTE_GS4, SIXTEENTH}, {NOTE_B4, SIXTEENTH},
// {NOTE_C5, QUARTER}, {NOTE_REST, SIXTEENTH},
// // -- Arpeggio 3 --
// {NOTE_E4, SIXTEENTH}, {NOTE_E5, SIXTEENTH}, {NOTE_DS5, SIXTEENTH},
// // -- Repeat Theme --
// {NOTE_E5, SIXTEENTH}, {NOTE_DS5, SIXTEENTH},
// {NOTE_E5, SIXTEENTH}, {NOTE_B4, SIXTEENTH},
// {NOTE_D5, SIXTEENTH}, {NOTE_C5, SIXTEENTH},
// {NOTE_A4, QUARTER}, {NOTE_REST, SIXTEENTH},
// // -- Ending 1 --
// {NOTE_C4, SIXTEENTH}, {NOTE_E4, SIXTEENTH}, {NOTE_A4, SIXTEENTH},
// {NOTE_B4, QUARTER}, {NOTE_REST, SIXTEENTH},
// // -- Ending 2 --
// {NOTE_E4, SIXTEENTH}, {NOTE_C5, SIXTEENTH}, {NOTE_B4, SIXTEENTH},
// {NOTE_A4, QUARTER} // Final Note
// };
// --- FUKASHIGI NO CARTE (Bunny Girl Senpai ED) --- BPM = 90
// const NoteEvent song[] PROGMEM = {
// {NOTE_REST, HALF + QUARTER},
// {NOTE_D5, TRIPLET}, {NOTE_A5, TRIPLET}, {NOTE_G5, TRIPLET},
// {NOTE_A5, QUARTER}, {NOTE_D5, TRIPLET}, {NOTE_A5, TRIPLET}, {NOTE_G5, TRIPLET},
// {NOTE_A5, QUARTER}, {NOTE_D5, TRIPLET}, {NOTE_A5, TRIPLET}, {NOTE_G5, TRIPLET},
// {NOTE_E5, QUARTER}, {NOTE_F5, QUARTER}, {NOTE_REST, EIGHTH}, {NOTE_C5, EIGHTH},
// {NOTE_D5, TRIPLET}, {NOTE_A5, TRIPLET}, {NOTE_G5, TRIPLET},
// {NOTE_A5, QUARTER}, {NOTE_D5, TRIPLET}, {NOTE_A5, TRIPLET}, {NOTE_G5, TRIPLET},
// {NOTE_C6, QUARTER}, {NOTE_AS5, QUARTER},
// {NOTE_A5, TRIPLET}, {NOTE_REST, TRIPLET}, {NOTE_E6, TRIPLET},
// {NOTE_E6, TRIPLET}, {NOTE_REST, TRIPLET}, {NOTE_F6, TRIPLET},
// {NOTE_C6, TRIPLET}, {NOTE_D6, TRIPLET}, {NOTE_C6, TRIPLET},
// {NOTE_D5, TRIPLET}, {NOTE_A5, TRIPLET}, {NOTE_G5, TRIPLET},
// {NOTE_A5, QUARTER}, {NOTE_C6, QUARTER}, {NOTE_A5, QUARTER}, {NOTE_G5, QUARTER},
// {NOTE_E5, QUARTER}, {NOTE_F5, QUARTER}, {NOTE_A5, QUARTER},
// {NOTE_D5, TRIPLET}, {NOTE_A5, TRIPLET}, {NOTE_G5, TRIPLET},
// {NOTE_A5, QUARTER}, {NOTE_D5, TRIPLET}, {NOTE_A5, TRIPLET}, {NOTE_G5, TRIPLET},
// {NOTE_F5, QUARTER}, {NOTE_CS5, QUARTER}
// };
// --- YORUSHIKA (FRIEREN) --- BPM = 204
// const NoteEvent song[] PROGMEM = {
// // Bass Intro / Riff
// {NOTE_B2, QUARTER}, {NOTE_F3, QUARTER}, {NOTE_A3, EIGHTH}, {NOTE_A3, EIGHTH},
// {NOTE_REST, QUARTER},
// {NOTE_F2, QUARTER}, {NOTE_E3, QUARTER}, {NOTE_E3, EIGHTH}, {NOTE_E3, EIGHTH},
// {NOTE_REST, QUARTER},
// {NOTE_G2, QUARTER}, {NOTE_F3, QUARTER}, {NOTE_A3, EIGHTH}, {NOTE_A3, EIGHTH},
// {NOTE_REST, QUARTER},
// {NOTE_D2, QUARTER}, {NOTE_F3, QUARTER}, {NOTE_A3, EIGHTH}, {NOTE_A3, EIGHTH},
// {NOTE_REST, QUARTER},
// {NOTE_B2, QUARTER}, {NOTE_A3, QUARTER}, {NOTE_C3, EIGHTH}, {NOTE_C3, EIGHTH},
// {NOTE_REST, QUARTER},
// {NOTE_F2, QUARTER}, {NOTE_E3, QUARTER}, {NOTE_E3, EIGHTH}, {NOTE_E3, EIGHTH},
// {NOTE_REST, QUARTER},
// {NOTE_G2, QUARTER}, {NOTE_F3, QUARTER}, {NOTE_A3, EIGHTH}, {NOTE_A3, EIGHTH},
// {NOTE_REST, QUARTER},
// // Melody Start
// {NOTE_D2, QUARTER}, {NOTE_F3, QUARTER}, {NOTE_D5, QUARTER}, {NOTE_E5, QUARTER},
// {NOTE_D5, HALF}, {NOTE_E5, QUARTER}, {NOTE_D5, QUARTER},
// {NOTE_C5, HALF}, {NOTE_F5, QUARTER}, {NOTE_E5, QUARTER},
// {NOTE_C5, QUARTER}, {NOTE_D5, HALF}, {NOTE_B4, QUARTER},
// {NOTE_B4, QUARTER}, {NOTE_D5, QUARTER}, {NOTE_D5, QUARTER}, {NOTE_E5, QUARTER},
// {NOTE_F5, HALF}, {NOTE_E5, QUARTER}, {NOTE_D5, QUARTER}
// };
// --- SETUP TIMER 1 ---
void setupTimer1() {
cli();
TCCR1A = 0; TCCR1B = 0; TCNT1 = 0;
OCR1A = TIMER_COMPARE;
TCCR1B |= (1 << WGM12) | (1 << CS10);
TIMSK1 |= (1 << OCIE1A);
sei();
}
void setup() {
pinMode(SHIFT_RCLK, OUTPUT);
digitalWrite(SHIFT_RCLK, LOW);
SPI.begin();
SPI.beginTransaction(SPISettings(8000000, LSBFIRST, SPI_MODE0));
setupTimer1();
}
//--- INTERRUPT SINE WAVE ---
// ISR(TIMER1_COMPA_vect) {
// if (idx == 256) idx = 0;
// uint8_t v = pgm_read_byte(&sine256[idx++]);
// SPI.transfer(v);
// PORTD |= (1 << 3);
// PORTD &= ~(1 << 3);
// }
// --- INTERRUPT ---
ISR(TIMER1_COMPA_vect) {
phaseAccumulator += phaseIncrement;
if (phaseIncrement == 0) {
// Silence
SPI.transfer(0);
} else {
uint8_t index = phaseAccumulator >> 8;
uint8_t val = pgm_read_byte(&sine256[index]);
SPI.transfer(val);
}
PORTD |= (1 << 3); // Latch High
PORTD &= ~(1 << 3); // Latch Low
}
// --- PLAYBACK FUNCTION ---
void playNote(uint8_t noteEnum, unsigned int durationMs) {
if (noteEnum == NOTE_REST) {
phaseIncrement = 0;
} else {
int freq = pgm_read_word(¬eFreq[noteEnum]);
// "unsigned long" (UL) to prevent overflow during the multiplication
phaseIncrement = (unsigned int)((freq * 65536UL) / SAMPLE_RATE);
}
delay(durationMs);
}
void loop() {
unsigned int BPM = 79;
unsigned int msPerBeat = 60000 / BPM;
int len = sizeof(song) / sizeof(song[0]);
for (int i = 0; i < len; i++) {
uint8_t n = pgm_read_byte(&song[i].note);
float b = pgm_read_float(&song[i].beats);
unsigned int duration = b * msPerBeat;
playNote(n, duration);
// Tiny gap
phaseIncrement = 0;
delay(10);
}
delay(2000);
} Sources
Michael Parchaiski
Basic Music Theory Concepts
https://youtu.be/AmC_qmSODEkMichael Parchaiski
Everything You Need to Know About Reading Music
https://youtu.be/JGGn5bEWCL4The Mento Zone
How to Count Basic Rhythms
https://youtu.be/YfT0irsgB3QMusescore
Music Sheets
https://musescore.com/element14 presents
Making a 12 bit DAC Using an Arduino
https://youtu.be/IDrWtgTb3D4bitluni
DAC using R-2R resistor ladder
https://youtu.be/_tABS7AX8D8Engineering Prof.
8-bit DAC Digital to Analog Converter Design
https://youtu.be/Y1H8HrrD4mQThe Headphone Show
What is a DAC and why do you need one?
https://youtu.be/HOvubnenXyoThe Headphone Show
What does an amplifier actually do?
https://youtu.be/--LEDxMs4ykThe Headphone Show
Audio amp classes as fast as possible!
https://youtu.be/d8ug0NbaIdsGreatScott
Class A Audio Amp Tutorial
https://youtu.be/7Pv-j_R3YP4AllAmericanFiveRadio
Transistor Push Pull Amplifier
https://youtu.be/e_SE4KQjYR8