Open Source
The Archean synthesizer is an open-source project. This means that all schematics and source code are freely available to the public on GitHub. Our goal is to invite the community to explore, modify, and improve the synthesizer together.

Source Code Overview
The source code is written for the Arduino IDE environment and runs on the Teensy 4.0 microcontroller. It controls the core functions of the synthesizer, including oscillator behavior, sensor inputs, MIDI communication, and audio generation. The code is structured into modules that manage different parts of the instrument, such as sound generation, envelope shaping (ADSR), LFO modulation, and user interaction through the capacitive touch keyboard and distance sensor.

How to Access and Edit the Code
You can access the full source code by visiting our GitHub repository. To start modifying the code:

- Clone or download the repository to your computer.
- Install the Arduino IDE and Teensy support (Teensyduino).
- Install required libraries: Adafruit_MPR121, VL53L0X and others listed in the documentation.
- Open the main project file (.ino) in the Arduino IDE.
- Familiarize yourself with the key modules and functions through the inline comments and documentation.
- Customize parameters like waveforms, sensor responsiveness, scale quantization, or MIDI settings to suit your preferences.
- Upload the modified code to the Teensy 4.0 using a USB connection.

Tips for Working with the Code
- Begin with small changes and test frequently to understand how your edits affect the sound and behavior.
- Use comments in the code as a guide—they explain the purpose of each section.
- Respect timing-critical sections (like OscillatorUpdate() interrupt)—changes here can cause audio glitches.
- Join our community forums or GitHub discussions if you need support or want to share improvements.
- Contributions through pull requests are welcome and help the project grow.
Archean.ino
#include "Adafruit_MPR121.h"
#include "TeensyTimerTool.h"
/*...*/
#include <AnalogBufferDMA.h>
In Arduino programming, #include statements are like importing tools into your workshop. They bring in pre-written code libraries that give your program special abilities. Without these, you'd have to write everything from scratch.

These libraries work together to give the Archean synthesizer its capabilities:
- Touch sensing for interactive controls
- Precise timing for accurate audio generation
- System stability with automatic crash recovery
- Fast audio generation using wavetables
- Musical intelligence with MIDI and frequency conversion
- Communication with other chips and devices
- Responsive controls by reading knobs and inputs efficiently

You'll notice some includes have quotes "Wavetable.h" and others have angle brackets <MIDI.h>:
- Quotes (`"..."`) - Custom files created specifically for this project
- Angle brackets (`<...>`) - Standard libraries that come with Arduino or Teensy

Both work the same way - they just tell the compiler where to look for the files.
#define OSC_DAC_CS_PIN  1
#define DAC_ADSR_AND_DISTANCE_CS  10
/*...*/
#define creates a constant - a name that represents a specific value throughout your code. Think of it as giving a friendly nickname to a number, so you don't have to remember what "pin 10" does every time you see it.

Why use defines?
- Makes code easier to read - GATE_PIN is clearer than just seeing 9
- Makes code easier to change - if you need to move a component to a different pin, you only change it in one place
- Prevents mistakes - if you type the name wrong, the compiler will warn you

When you see these defines used later in the code, like:
digitalWrite(GATE_PIN, HIGH);

Remember that it's the same as writing:
digitalWrite(9, HIGH);

But GATE_PIN tells you what you're controlling, not just which pin you're using!
// Global variables
volatile byte SPIfree = true;
volatile byte I2Cfree = true;
boolean Gate = false;
/*...*/
Global variables are like a whiteboard in a shared workspace - anyone in the program can read from it or write to it at any time. They store information that multiple parts of your code need to access.

Important concept: Variables declared outside of functions are "global" and can be used anywhere in your program.

What does `volatile` mean?
It tells the compiler: "This variable can change at ANY moment, even when you don't expect it - so ALWAYS check its real value, never assume!"

Why is this important?
In synthesizers, interrupts can change variables at unpredictable times. Without volatile, the compiler might "optimize" your code by assuming the variable doesn't change, which could cause bugs.

Data Types:
boolean
- Can only be true or false
- Uses 1 byte of memory
- Perfect for yes/no questions

byte
- Can be 0 to 255
- Uses 1 byte of memory
- Often used for flags (0 = false, 1 = true, or LOW/HIGH)
- More flexible than boolean if you need multiple states

short
- Short integer (16-bit signed integer)
- Can be -32,768 to +32,767
- Often used for MIDI note numbers (0-127, fits easily), scale numbers (small positive integers), counter values, small integer calculations

// Timers 
IntervalTimer OscillatorTimer; // Oscillator
PeriodicTimer ADSRtimer; // ADSR
PeriodicTimer LFOtimer; // LFO
/*...*/
Imagine the Archean synthesizer is an orchestra. You have different sections:

- Oscillators playing the main note.
- ADSR shaping the volume of each note.
- LFO adding wobbles and pulsations and more..

If every musician played at their own random speed, it would be chaos! They need a conductor to keep everyone in time. Timers are the conductors of your synthesizer.

What is a Timer?
In code, a timer is a special function that says: "Do this specific task, over and over, at a very precise speed."

The Teensy 4.0 brain is incredibly fast, but sound requires perfect timing. If the timing is off, the sound will crackle, warble, or stop altogether. Timers make sure every part of the synth gets the attention it needs, exactly when it needs it.

We need timers to keep the different parts of the synth in perfect sync, running each part at its own ideal speed to create clean and stable sound.

// Watching dog
WDT_T4<WDT1> WDT;
Imagine you're running a very important, live musical performance with the Archean. The code has to keep running no matter what. But what if something goes wrong? What if a bug in the code, some electrical noise, or a weird knob twist causes the Teensy brain to get stuck in an infinite loop or just freeze?

This is where the Watchdog comes in.

The "Feed the Dog" Analogy

Think of the Watchdog as a very loyal, but very hungry, guard dog you have assigned to protect your synthesizer.

1. You tell the dog: "Watch my program. If I don't check in with you and give you a virtual 'dog treat' every second, it means I'm stuck and you need to reboot the entire system to save me."
2. In your main loop, the code regularly "feeds the dog" by calling a function WDT.feed();. This is the dog treat. It tells the watchdog, "Everything is fine! I'm still running normally."
3. If the code gets stuck (for example, in a while loop that never ends, or it crashes), it can no longer feed the dog.
4. The watchdog gets hungry and angry. After exactly one second of no treats, it decides the program is dead. It then does the only thing it can to save the situation: it barks, and that bark is a full system reboot. The Teensy restarts, your sketch starts running from the beginning again, and the synth comes back to life.

Why is this so important for the Archean?
- Prevents a "Frozen Synth": Without a watchdog, if your code crashed, the synth might just hang, producing a loud, continuous tone (a "stuck note") or just go silent. You'd have to manually power it off and on.
- Great for Beginners: When you are learning to code and writing new patches, you will inevitably write a bug that crashes the system. The watchdog acts as an automatic recovery system, saving you from countless manual reboots and making the development process much smoother.
- Professional Reliability: For a finished instrument, it ensures that even in the case of a very rare and unexpected glitch, the synth will recover on its own within a second, rather than becoming a useless brick until the user intervenes.
void setup(){
    /* .... */
}
Imagine you're a musician setting up your gear on stage before a concert.

The setup() function is your synthesizer's "sound check" and setup routine.

What is the setup() function?
In the Arduino IDE, every program (called a "sketch") must have two special functions: setup() and loop(). The setup() function runs only one time, immediately when you turn on the synthesizer or press the reset button.

Its job is to prepare the Teensy brain and all the hardware to be ready to make sound and read your controls.

void setup() {
// The commands inside these curly braces { } run ONLY ONCE at startup.
}

What Do We Put Inside setup() for the Archean?
Inside the setup() function, you give the Teensy all its initial instructions. For the Archean, this typically includes:

Category Example Code What it's doing (The "Sound Check").

Communication Serial.begin(9600); Turns on the USB connection to your computer, so the synth can send messages for debugging.
Pin Configuration pinMode(LED_PIN, OUTPUT); Tells the Teensy, "Get ready, pin number 8 is going to be used to light up an LED."
Starting Timers OscillatorTimer.begin(OscillatorUpdate, 44000); Starts the "conductors" we talked about, telling them how fast to run and which function to call.

In short: The setup() function is where you write all the startup instructions that prepare the Archean's hardware and software to be a musical instrument. After setup() finishes, the loop() function immediately takes over to handle the continuous playing and interaction.
// LED blinks once at startup
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH);
delay(10);
digitalWrite(LED_PIN, LOW);
The "Hello, World!" Blink: The Synthesizer's Power-On Signal

This small section of code is the synthesizer's way of saying, "I'm alive and everything is working!" It's like when you turn on a car and all the dashboard lights briefly illuminate for a check.

Step-by-Step Breakdown:
1. pinMode(LED_PIN, OUTPUT);

- What it does: This is an instruction for the Teensy brain.
- In simple terms: It tells the Teensy, "See that physical pin connected to the LED? I want to use it to send out electricity (to light the LED), not to read in signals (like from a button)." Think of it as setting a switch to "TRANSMIT" instead of "RECEIVE."

2. digitalWrite(LED_PIN, HIGH);

- What it does: This is an action.
- In simple terms: It commands the Teensy, "Now, send power (HIGH) to the LED pin!" This is the equivalent of flipping a light switch ON. The LED receives power and lights up.

3. delay(10);

- What it does: This tells the Teensy to pause and wait.
- In simple terms: "Keep the LED on for 10 milliseconds." A millisecond is 1/1000th of a second, so this is a very short, quick blink - just long enough for your eye to see it. Without this delay, the LED would turn on and off so fast you might not even see it!

4. digitalWrite(LED_PIN, LOW);

- What it does: This is another action.
- In simple terms: This command tells the Teensy, "Okay, now stop sending power (LOW) to the LED pin." This is like flipping the light switch OFF. The LED turns off.

Why is this in the setup()?
This blink happens in the setup() function because it's a one-time startup ritual. It serves two main purposes:

1. For You (The User): It gives you a clear, visual confirmation that the synth has power and the Teensy brain has successfully started running your program. It's a quick "thumbs up" from your instrument.
2. For You (The Programmer): It's a fantastic debugging tool. If you upload new code and the LED doesn't blink, you know immediately that the program isn't even getting past the very beginning of the setup() function, which helps you narrow down where a problem might be.
MIDIinit();
This function calls initializes (sets up) the MIDI system. It prepares the synthesizer to send and receive MIDI messages.

What is MIDI?
MIDI = Musical Instrument Digital Interface.
A communication protocol (language) that lets musical instruments, computers, and synthesizers talk to each other.

What does it send?
MIDI doesn't send audio - it sends instructions like:
- "Play note 60 (middle C) at velocity 100 (loudness)"
- "Stop playing note 60"
- "Turn knob 7 to position 64"
- "Bend the pitch up by 200 cents"
SPI.begin();
This function initializes the SPI communication system. It prepares the Teensy to talk to external chips using the SPI protocol.

What is SPI?
SPI = Serial Peripheral Interface.

A very fast communication protocol that lets the Teensy (master) talk to other chips (slaves) - like DACs.

Key characteristics:
- Fast - Can transfer millions of bits per second
- Synchronous - Uses a clock signal to coordinate timing
- Full duplex - Can send and receive data simultaneously
- Master-Slave - One device (Teensy) controls the communication

Why Does the Archean Use SPI?
The Archean has three DACs (Digital to Analog Converters) that need to receive digital data from the Teensy and convert it to analog voltages:

1. Oscillator DAC - Generates the main audio signal
2. ADSR & Distance DAC - Creates envelope and sensor control voltages
3. LFO & Element DAC - Generates modulation signals

All three DACs communicate via SPI.

SPI uses four connections (though some devices only need three):

1. MOSI - Master Out, Slave In
What it does: Carries data FROM the Teensy TO the DACs
Example: Teensy sends "output 2.5 volts" to the oscillator DAC

2. MISO - Master In, Slave Out
What it does: Carries data FROM the slaves TO the Teensy
Note: DACs usually don't send data back, so this might not be used in Archean

3. SCK - Serial Clock
What it does: Provides timing pulses so sender and receiver stay synchronized
Analogy: Like a metronome that keeps everyone in sync

4. CS - Chip Select (also called SS - Slave Select)
What it does: Tells specific chips "I'm talking to YOU now"
Why needed: Multiple devices share MOSI/MISO/SCK, so CS selects which one listens

Think of it like this:
- MOSI, MISO, SCK = shared phone line
- CS = dialing a specific extension to reach one person

pinMode(GATE_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(GATE_PIN), GateInterrupt, CHANGE);
/*...*/
What is an Interrupt?
An interrupt is like a doorbell for your microcontroller - it immediately stops what it's doing to handle something urgent, then returns to what it was doing before.

Real-world analogy:
You're reading a book (main program loop). Suddenly, your phone rings (interrupt). You:
1. Stop reading and remember your page
2. Answer the phone (interrupt handler)
3. Finish the call
4. Return to your book exactly where you left off

Why interrupts are important in synthesizers:
- Instant response - No waiting for the main loop to check inputs
- Real-time performance - Critical for musical timing
- Efficiency - CPU does other work until something needs attention

Let's examine each section of the interrupt setup:

Part 1: pinMode() - Configuring the Pin
pinMode(KEYBOARD_INTERRUPT_PIN, INPUT_PULLUP);

What is pinMode()?
Tells the Teensy how a specific pin should behave.

Understanding INPUT_PULLUP
This is the most confusing part for beginners! Let's break it down:

The Problem Without Pull-up:
When a pin is set to INPUT and nothing is connected, it "floats" - it picks up electrical noise and gives random readings. It's like an antenna picking up radio static.

The Solution - Pull-up Resistor:
A pull-up resistor connects the pin to +3.3V through a high-value resistor (typically 10kΩ). This:
- Keeps the pin at HIGH by default
- Prevents floating/noise
- Goes LOW only when actively grounded

Part 2: attachInterrupt() - Setting Up the Interrupt
attachInterrupt(digitalPinToInterrupt(GATE_PIN), GateInterrupt, CHANGE);

What is attachInterrupt()?
Tells the Teensy: "When something happens on this pin, immediately run this function!"

Syntax:
attachInterrupt(interrupt_number, function_to_call, trigger_mode);

Parameter 1: digitalPinToInterrupt(GATE_PIN)
What it does: Converts a pin number to its interrupt number.

Why needed? Some older Arduino boards have different numbers for pins vs interrupts. This function makes code portable.

Example:
digitalPinToInterrupt(9)→ Returns the interrupt number for pin 9

On Teensy 4.0: Almost every pin can be an interrupt! This is much better than older Arduinos which only had 2-6 interrupt pins.

Parameter 2: GateInterrupt
What it is: The name of the function to call when interrupt happens.

Example function:
void GateInterrupt(){
// This code runs IMMEDIATELY when GATE_PIN changes
// Keep it SHORT and FAST!
Gate = digitalRead(GATE_PIN);
}

CRITICAL RULES for Interrupt Functions:
1. Be FAST - Get in, do minimal work, get out
2. No delays - Never use delay() inside
3. Use volatile variables - So main loop sees changes
4. Set flags - Let main loop do heavy work
5. No serial prints - Too slow for interrupts

Parameter 3: CHANGE
What it is: The trigger mode - WHEN should the interrupt fire?

Available Modes:
LOW, HIGH, CHANGE Pin changes (HIGH→LOW or LOW→HIGH), RISING (Pin goes LOW→HIGH), FALLING (Pin goes HIGH→LOW)

In Archean, `CHANGE` is used because:
- We want to know when sliders START moving (LOW→HIGH)
- We want to know when sliders STOP moving (HIGH→LOW)
- Both directions matter!

Without interrupts: The synth would feel sluggish and miss fast musical gestures.
With interrupts: Every touch, every slider move, every gate pulse is captured instantly!
Wire1.begin();
Wire1.setClock(3400000);
What is Wire?
Wire is the Arduino library name for I2C (Inter-Integrated Circuit) communication.
Why is it called "Wire"? Because I2C only uses 2 wires to communicate with multiple devices - simple and elegant!

What is I2C?
I2C = Inter-Integrated Circuit (sometimes called I²C or IIC)
I2C is a communication protocol that lets multiple chips talk to each other using just two wires.

Key characteristics:
- 2 wires only - SDA (data) and SCL (clock)
- Multiple devices - Up to 127 devices on same bus
- Master-Slave - Teensy is master, other chips are slaves
- Addressable - Each device has a unique address
- Medium speed - Slower than SPI, but uses fewer wires

Why "Wire1" and not just "Wire"?
The Teensy 4.0 has multiple I2C buses:
- Wire → I2C bus 0 (pins 18 and 19)
- Wire1 → I2C bus 1 (pins 16 and 17)
- Wire2 → I2C bus 2 (pins 24 and 25)

Part 1: Begin
Wire1.begin();

What it does:
Initializes I2C bus 1 and prepares it for communication.

Specifically, it:
1. Configures the pins:
- Pin 17 (SDA1) → Data line (bidirectional)
- Pin 16 (SCL1) → Clock line (output)

2. Sets default speed:
- 100 kHz (standard mode) initially

3. Enables pull-up resistors:
- Both SDA and SCL need pull-ups to work
- Often external resistors (4.7kΩ typical), but Teensy has internal ones too

4. Activates I2C hardware:
- Turns on the Teensy's I2C controller
- Ready to send/receive data

Part 2: Set Clock
Wire1.setClock(3400000);

What it does:
Sets the I2C communication speed to 3.4 MHz (3,400,000 Hz).
3.4 MHz = High-Speed Mode - The fastest standard I2C speed!

Why 3.4 MHz for the Touch Keyboard?
The Archean uses the MPR121 capacitive touch sensor chip for its keyboard.

Reasons for high speed:
1. Fast response time:
- Musicians need instant feedback
- 3.4 MHz reads all 12 touch inputs quickly
- Reduces latency between touch and sound

2. Multiple reads per second:
- Touch sensing requires frequent scanning

High speed allows 1000+ scans per second
- Smooth, glitch-free playing experience

3. MPR121 supports it:
- The chip is rated for high-speed I2C
- Why use slow speed if the hardware can go faster?
void loop(){
    /* ... */
    // Set oscillator frequency
    Pitch();
   
    /* ... */
    // Checking button state
    CheckButtonState();

    /* ... */
    // Feeding the Watchdog Timer to prevent system reset
    WDT.feed();
}
If the setup() function was the "sound check," then the loop() function is the live performance that never ends.

What is the loop() function?
The loop() function runs continuously, over and over again, from the moment setup() finishes until you turn off the power. It's the main program where your synth:

- Listens to your commands
- Updates its settings
- Makes decisions
- Creates sound

The Magic of "Fast Enough"
A common beginner question is: "If the loop runs millions of times per second, won't it be too fast?"

The key insight is that humans perceive things much slower than computers. The loop runs so fast that to us, it feels like the synth is doing everything instantly and simultaneously. Turning a knob changes pitch immediately Pitch() runs thousands of times, catching your knob movement. Pressing a button works instantly CheckButtonState() detects the press within microseconds. Smooth, continuous sound The loop keeps everything updated between timer interrupts.

Summary:
- void loop() is the main program that runs forever.
- It checks everything repeatedly: knobs, buttons, sensors.
- It feels instantaneous because the loop runs incredibly fast.
- It works with the timers: The loop handles "slow" stuff (knobs, buttons) while timers handle "fast" stuff (actual sound generation).

The Archean's loop is efficiently checking: "What should the pitch be? Are any buttons pressed? Am I still running okay?" - thousands of times every second!
1V_OCT.h
// Values are in microseconds
double V_OCT[] = { 
  238.891028,
  237.759720,
  236.633769 
  /* ... */
}
What is 1V/OCT?
1V/OCT = 1 Volt per Octave

This is the standard used in modular synthesizers and analog synths to control pitch using voltage.
Each increase of 1 volt in control voltage raises the pitch by exactly 1 octave (frequency doubles).

Example:
- 0V → C1 (32.70 Hz)
- 1V → C2 (65.41 Hz) - one octave higher, double the frequency
- 2V → C3 (130.81 Hz) - another octave, frequency doubled again
- 3V → C4 (261.63 Hz) - middle C

Why 1V/OCT Matters?
Musical Context:
In modular synthesis, you control pitch with voltage instead of pressing keys:
- Turn a knob → Changes voltage → Changes pitch
- Sequencer outputs voltage → Synth plays that pitch
- Keyboard sends voltage → Synth plays that note

Universal Standard:
Almost all modern modular synths use 1V/OCT:
- Eurorack modules - 1V/OCT
- Buchla systems - Used to use 1.2V/OCT (older standard)
- Moog modular - 1V/OCT
- CV/Gate sequencers - 1V/OCT

This means: Archean can be controlled by any standard modular gear!

What This Array Contains:
This is a lookup table that converts voltage readings into timer periods.

Breaking it down:
- Array name: V_OCT[ ] - Voltage per Octave lookup table.
- Data type: double - Floating point numbers for precision.
- Units: Microseconds (µs) - Timer periods.
- Purpose: Convert ADC readings (representing voltage) into oscillator frequencies.

Let's understand the chain of conversions:

The Complete Signal Flow:
Input Voltage → ADC Reading → Array Index → Timer Period → Oscillator Frequency

Step-by-Step Example:
Step 1: Input Voltage
External CV jack receives 2.5V (representing a musical note)

Step 2: ADC Converts to Digital
ADC reads 2.5V → Returns value 512 (out of 0-1023)

Step 3: Look Up Timer Period
V_OCT[512] → Returns 181.5705235 microseconds

Step 4: Set Timer
Timer triggers every 181.5705235 µs
This generates a frequency of about 5507.5 Hz

Step 5: Generate Audio
Timer interrupt fires → Read next wavetable sample → Output to DAC
Result: You hear a specific musical pitch!

Why Use Microseconds?
Frequency tells you how many cycles per second
Period tells you how long one cycle takes

Mathematical relationship:
Frequency (Hz) = 1,000,000 / Period (µs)
Period (µs) = 1,000,000 / Frequency (Hz)

Example Calculations:
For Middle C (261.63 Hz):
Period = 1,000,000 / 261.63
Period = 3,822.2 µs

For C one octave higher (523.25 Hz):
Period = 1,000,000 / 523.25
Period = 1,911.1 µs (half the period = double the frequency!)

Why Store Periods Instead of Frequencies?
The Teensy's timer hardware works with periods, not frequencies:
// Easy with period values:
timer.setPeriod(V_OCT[adcValue]); // Direct lookup!

// Harder with frequency values:
timer.setPeriod(1000000 / FREQ_TABLE[adcValue]); // Needs division every time

Division is slow! Pre-calculating periods and storing them in a table is much faster for real-time audio.
ADC.ino
ADC *adc = new ADC();

const uint32_t InitialAverageValue = 2;
const uint32_t BufferSize = 10;

DMAMEM static volatile uint16_t __attribute__((aligned(32))) dma_adc_buff1[BufferSize];
DMAMEM static volatile uint16_t __attribute__((aligned(32))) dma_adc_buff2[BufferSize];
AnalogBufferDMA abdma(dma_adc_buff1, BufferSize, dma_adc_buff2, BufferSize);
What is ADC?
ADC = Analog to Digital Converter

The Problem:
- The real world is analog (continuous voltages)
- Computers are digital (only understand numbers: 0s and 1s)
- We need a bridge between these two worlds!

What ADC Does:
Converts continuous analog voltage into discrete digital numbers.

In the Archean Synthesizer:
The ADC reads voltages from:
- CV inputs - What pitch voltage is coming in?
- Potentiometer positions - Tune, Fine, ADSR , etc.

ADC Resolution
Teensy 4.0 ADC Specifications:
- Resolution: 12-bit (can be set to 10-bit for speed)
- Range: 0V to 3.3V
- Output values: 0 to 4095 (12-bit) or 0 to 1023 (10-bit)

What This Means:
12-bit ADC:
0V → 0
1.65V → 2048 (middle)
3.3V → 4095 (maximum)

Precision: 3.3V / 4096 steps = 0.8 mV per step
That's incredibly precise - you can detect voltage changes smaller than a thousandth of a volt!

Part 1: Creating the ADC Object
ADC *adc = new ADC();

Breaking It Down:
`ADC` - The class/type (comes from ADC library)
`*adc` - A pointer to an ADC object
`new ADC()` - Creates a new ADC object in memory

What This Does:
- Creates and initializes the ADC hardware controller
- Configures ADC settings (resolution, speed, averaging)
- Makes it ready to read analog inputs

Part 2: Configuration Constants
const uint32_t InitialAverageValue = 2;
const uint32_t BufferSize = 10;
InitialAverageValue = 2

What it means: Average 2 ADC readings together

Why averaging?
- Analog signals have noise (random fluctuations)
- Single readings can be unstable
- Averaging smooths out the noise

Example without averaging:
Single readings: 512, 514, 511, 515, 510 (jumpy!)

With averaging (2 samples):
Averaged: 513, 512.5, 513, 512.5 (smoother!)

Trade-off: More averaging = smoother but slower

Why only 2? Balance between noise reduction and speed - we need fast response for musical performance!

BufferSize = 10;
What it means: Each DMA buffer holds 10 ADC readings

Why 10?
- Small enough for low latency (fast response)
- Large enough for efficient DMA transfers
- Good balance for real-time audio

What happens with these 10 readings?
Buffer fills: [sample1, sample2, sample3, ..., sample10]
DMA interrupt fires: "Buffer full! Process these!"
While processing buffer 1, buffer 2 is being filled

This is called double buffering - we'll explain it soon!

Part 3: What is DMA?
DMA = Direct Memory Access

The Problem Without DMA:

Traditional ADC reading:
void loop() {
int value = analogRead(A0); // CPU stops and waits for ADC
// CPU is BLOCKED during conversion
// Can't do anything else!
}

CPU workflow:
1. CPU tells ADC: "Start conversion"
2. CPU WAITS (doing nothing) ← WASTEFUL!
3. ADC finishes
4. CPU reads result
5. Repeat...

For a synthesizer processing audio in real-time, this is TERRIBLE!
The Solution: DMA

With DMA:
1. CPU tells DMA controller: "Read ADC and store results in this buffer"
2. CPU goes back to making music!
3. DMA controller handles ALL the ADC transfers independently
4. When buffer is full, DMA interrupts CPU: "Data ready!"
5. CPU processes the data

Part 4: Understanding the Buffer Declarations
DMAMEM static volatile uint16_t __attribute__((aligned(32))) dma_adc_buff1[BufferSize];
DMAMEM static volatile uint16_t __attribute__((aligned(32))) dma_adc_buff2[BufferSize];

This looks scary, but let's decode each part:

DMAMEM
What it means: Store this in special DMA-accessible memory

Why needed?
- Not all RAM is accessible to DMA controller
- DMAMEM ensures buffers are in the right memory region
- On Teensy 4.0, this places data in specific memory banks

Without DMAMEM: DMA transfer would fail or crash!

static
What it means: Variable persists for the entire program lifetime

Why needed?
- DMA needs buffers to exist continuously
- Not created/destroyed with function calls
- Always at the same memory address

volatile
What it means: "This variable can change at any moment - always check its real value!"

Why needed?
- DMA hardware modifies these buffers independently
- Compiler might optimize and cache values
- volatile forces compiler to always read from actual memory

Remember this from earlier? Same reason we used volatile for interrupt variables!

uint16_t
What it means: Unsigned 16-bit integer (0 to 65,535)

Why 16-bit?
- Teensy ADC produces 12-bit values (0-4095)
- 16-bit provides room for averaging or processing
- Aligns nicely with memory architecture

Memory per sample: 2 bytes

__attribute__((aligned(32)))
What it means: Align this array to 32-byte boundary in memory

Why needed?
- DMA controller is faster with aligned memory
- Some DMA operations REQUIRE alignment
- Prevents crashes and corruption

Technical: Memory address must be divisible by 32

Example:
Good: Address 0x20000000 (divisible by 32) ✓
Good: Address 0x20000020 (divisible by 32) ✓
Bad: Address 0x20000015 (not divisible by 32) ✗

dma_adc_buff1[BufferSize] and dma_adc_buff2[BufferSize]
What it means: Two arrays, each holding 10 samples

Why TWO buffers? → DOUBLE BUFFERING!

The Problem with Single Buffer:
CPU reading buffer → DMA trying to write → CONFLICT! ✗

The Dance:
1. DMA fills buffer 1 while CPU processes buffer 2
2. When buffer 1 is full, DMA switches to buffer 2
3. CPU processes buffer 1 while DMA fills buffer 2
4. Repeat forever!

Result: No waiting, no blocking, continuous data flow!

Part 5: AnalogBufferDMA Object
AnalogBufferDMA abdma(dma_adc_buff1, BufferSize, dma_adc_buff2, BufferSize);

What This Does:
Creates a DMA controller object that manages the double-buffered ADC reading.

Parameters:
AnalogBufferDMA abdma(
dma_adc_buff1, // First buffer pointer
BufferSize, // First buffer size (10)
dma_adc_buff2, // Second buffer pointer
BufferSize // Second buffer size (10)
);

The Object Handles:
- Setting up DMA channels
- Configuring ADC triggers
- Managing buffer swapping
- Signaling when data is ready
- All the complex DMA hardware details

You just call simple functions, and it handles all the complexity!

Measuring the Difference:
Without DMA (blocking):
Reading 10 samples: ~100 microseconds of CPU time BLOCKED
Doing this 1000 times per second: 100ms of CPU time wasted
That's 10% of CPU doing nothing but waiting!

With DMA:
Reading 10 samples: ~1 microsecond of CPU time (just setup)
DMA does the work: 0 CPU time
CPU free to generate audio: 99% efficiency!
class LowPass
{
  private:
    float a[order];
    float b[order+1];
    float omega0;
    /*...*/
}
What is This Code For?
This filter cleans up noisy signals from analog inputs like potentiometers and CV inputs.
We won't explain HOW the math works - just WHY we need it and HOW to use it!

The Problem: Noisy ADC Readings

What is Noise?
When you read analog inputs with an ADC, you don't get perfectly smooth values. Instead, you get readings that jump around randomly:

Without filtering:
Knob at 50% position should read: 2048

Actual ADC readings:
2048, 2051, 2045, 2049, 2052, 2046, 2050, 2047, 2051, 2048

Sources of Noise:
1. Electrical interference - Power supplies, WiFi, motors, other circuits
2. Thermal noise - Heat causes random electron movement
3. Quantization noise - ADC can only represent discrete values
4. Ground loops - Different ground potentials in the circuit
5. EMI (Electromagnetic Interference) - Radio waves, cell phones, etc.

Visual representation:
Perfect signal: ──────────── (smooth line)

Noisy signal: ─╱─╲╱─╲─╱─╲─ (jittery, unstable)

1. Audio Artifacts
// Noisy knob controlling pitch
noisy value: 2048 → 2051 → 2045
frequency: 440Hz → 441Hz → 439Hz
Result: You hear random pitch wobbles and glitches even when the slider isn't moving!

2. Unwanted Modulation

3. Zipper Noise
When control values change abruptly, you hear clicking or "zipping" sounds in the audio - very unprofessional!

The Solution: Low Pass Filter

What Does "Low Pass" Mean?
A Low Pass Filter (LPF) allows low-frequency changes through while blocking high-frequency noise.

Musical analogy: Like turning down the treble knob on a stereo - you keep the bass (slow changes) and remove the highs (fast noise).

For control signals:
- Low frequency = You slowly moving a knob
- High frequency = Electrical noise jittering rapidly
/*...*/
adc->adc0->setAveraging(1); // set number of averages
adc->adc0->setResolution(10); // set bits of resolution  
adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED); // fastest conversion
adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED); // fastest sampling
/*...*/
These lines configure how the ADC operates. Let's break down each one!

Understanding adc->adc0
adc->adc0->setSomething();

What this means:
- adc - The ADC object we created earlier.
- adc0 - The first ADC module (Teensy 4.0 has two: ADC0 and ADC1).
- -> - Pointer access (accessing object through pointer).

Why adc0?
Teensy 4.0 has two independent ADC modules that can work simultaneously:
- ADC0 - First ADC (certain pins)
- ADC1 - Second ADC (different pins)

1. setAveraging(1);
adc->adc0->setAveraging(1); // set number of averages

What It Does:
Controls how many samples the ADC takes and averages together for each reading.

Options:
setAveraging(1); // No averaging - fastest, but noisiest
setAveraging(4); // Average 4 samples - balanced
setAveraging(8); // Average 8 samples - smoother
setAveraging(16); // Average 16 samples - smoothest, but slowest
setAveraging(32); // Maximum averaging

Why Archean Uses 1:
Reason: Maximum speed for real-time performance!

How it works:
With averaging = 1:
Read once → Return immediately (fastest!)

With averaging = 4:
Read 4 times → Calculate average → Return (4x slower)

Remember: Archean already uses a software Low Pass Filter (from the previous section) to remove noise, so hardware averaging isn't needed. This gives the best of both worlds:
- Fast ADC readings
- Smooth values from software filter

2. setResolution(10);
adc->adc0->setResolution(10); // set bits of resolution

What It Does:
Sets how many bits the ADC uses to represent voltage values.

Why Archean Uses 10-bit?
Perfect balance for synthesizer controls:
- Good enough precision: 3.2 mV steps are more than adequate for sliders and CV inputs
- Faster conversion: ~2x faster than 12-bit
- Less data: Smaller numbers to process
- Matches MIDI resolution: MIDI uses 7-bit (0-127) and 14-bit (0-16383) for controls

3. setConversionSpeed();
adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);

What It Does:
Controls how fast the ADC converts an analog voltage to a digital number.

Conversion = The actual analog-to-digital conversion process

4. setSamplingSpeed();
adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);

What It Does:
Controls how fast the ADC captures (samples) the input voltage before converting it.

Sampling = Capturing the voltage level (happens before conversion).
Every microsecond saved = better musical experience!

This is embedded systems engineering at its best: understanding the requirements (musical performance), making the right trade-offs (speed over maximum precision), and configuring hardware to match the application perfectly.
ADSR.ino
void ADSRinterrupt(void){
  if(oldEnvelope != Envelope){
    if(SPIfree == true && PIT_CVAL0 > 15){
      SPIfree = false;
      DAC(Envelope, DAC_ADSR_AND_DISTANCE_CS, false);
      SPIfree = true;
      oldEnvelope = Envelope;
    }
  }
  ADSRInterruptState = HIGH;
}
This interrupt function updates the ADSR envelope voltage whenever the envelope value changes.

ADSR = Attack, Decay, Sustain, Release - shapes how a sound evolves over time

Step-by-Step Breakdown
1. Check if Envelope Changed
if(oldEnvelope != Envelope)
Purpose: Only update the DAC if the envelope value actually changed
Why: Saves CPU time and SPI bus bandwidth - no point sending the same value repeatedly

2. Check if SPI is Available
if(SPIfree == true && PIT_CVAL0 > 15)
Two safety checks:
- SPIfree == true - Make sure SPI bus isn't being used by another process
- PIT_CVAL0 > 15 - Timer check to prevent conflicts (PIT = Periodic Interrupt Timer)

Why both? Prevents SPI collisions that would corrupt data

3. Send Envelope to DAC
SPIfree = false; // Lock SPI bus
DAC(Envelope, DAC_ADSR_AND_DISTANCE_CS, false); // Send value to ADSR DAC
SPIfree = true; // Release SPI bus
oldEnvelope = Envelope; // Remember this value

What happens:
1. Mark SPI as busy (prevent others from using it)
2. Send the envelope value to the ADSR DAC via SPI
3. Mark SPI as free again
4. Save current envelope value to compare next time

4. Set Interrupt Flag
ADSRInterruptState = HIGH;
Purpose: Signals the main loop that the interrupt has run
Usage: Main program can check if envelope update happened

Why This Design?
Problem: Envelope Changes Constantly
During Attack/Decay/Release phases, the envelope value changes every few milliseconds. If we updated the DAC in the main loop, we might miss updates or cause timing issues.

Solution: Interrupt-Driven Updates
The interrupt fires regularly (timer-based), checks if envelope changed, and immediately updates the DAC if needed.

Result: Smooth, precise envelope control without blocking the main program

Why Check PIT_CVAL0 > 15?
PIT_CVAL0 is a timer countdown register.

The check `PIT_CVAL0 > 15` ensures:
- We're not too close to the next timer interrupt
- Gives SPI transmission time to complete safely

Think of it as: "Make sure there's enough time before the next interrupt fires"
void ADSR(void){  
  // Note on
  if(Gate == true){
    // ATTACK
    if(DecayState == false){
      /*...*/
    }
    // DECAY
    if(Envelope < 95 && DecayState == false){
      /*...*/
    }
    // Sustain
    if(DecayState == true && Envelope <= map(Sust, 0, 255, 4095, 0)){ 
      /*...*/
    }
  }
  
  // Note Off
  if(Gate != true){ 
    // RELEASE
    if(NoteIsActive == true){
      /*...*/
    }
  }
}
What is ADSR?
ADSR shapes how a sound evolves over time:
- Attack - How fast the sound reaches full volume
- Decay - How fast it drops to sustain level
- Sustain - The held volume level
- Release - How fast it fades after releasing the key

Function Structure
Two Main States:
1. Gate ON (Note pressed):
- Attack → Decay → Sustain
2. Gate OFF (Note released):
- Release

Note On: Gate == true

Attack Phase
if(DecayState == false) {
/*...*/
}

When: Note first pressed, envelope rising
What happens: Envelope increases rapidly toward maximum (4095)
Ends when: Envelope reaches ~95% (threshold = 95)

Decay Phase
if(Envelope < 95 && DecayState == false) {
/*...*/
}

When: Attack finished, envelope decreasing
What happens: Envelope decreases from maximum toward sustain level
Condition: Envelope < 95 means we're past the attack peak
Ends when: Envelope reaches the sustain level

Sustain Phase
if(DecayState == true && Envelope <= map(Sust, 0, 255, 4095, 0)) {
/*...*/
}

When: Decay finished, key still held
What happens: Envelope holds steady at sustain level
Sustain level: Mapped from parameter Sust (0-255) to envelope range (4095-0)
Lasts: As long as the key is held down

Note Off: Gate != true

Release Phase
if(NoteIsActive == true) {
/*...*/
}

When: Key released
What happens: Envelope decreases from current level to 0
Check: NoteIsActive ensures we only release if a note was playing
Ends when: Envelope reaches 0 (silent)
Button.ino
void CheckButtonState(void){
  ButtonState = !digitalRead(BUTTON_PIN); // HIGH is Pressed!
}
The Button.ino file contains all the code related to reading the physical button on the Archean synthesizer. This is how the synth knows when you're pressing the button to change sounds, activate effects, or switch modes.

This function has one job: check if the button is currently pressed or not.

1. digitalRead(BUTTON_PIN)
- What it does: This is an Arduino function that reads the electrical state of a specific pin.
- In simple terms: It checks the button and reports back: HIGH (usually means voltage is present) or LOW (usually means no voltage).
- How buttons work electrically: When a button is not pressed, the pin might read HIGH. When you press the button, it connects the pin to ground, making it read LOW.

2. The ! (NOT operator)
- What it does: This flips the value. If digitalRead returns HIGH, ! makes it LOW, and vice versa.
- Why we need it: The comment // HIGH is Pressed! tells us we want ButtonState to be HIGH when the button is pressed. Since the physical wiring might give us LOW when pressed, we use ! to invert it.

3. ButtonState
- What it does: This stores the result (either HIGH or LOW) in a variable called ButtonState.
- Why we need it: Other parts of the code can now check ButtonState to see if the button is pressed, without having to read the physical button again.

Every time through the loop, the synth quickly:
1. Calls CheckButtonState();
2. The function reads the button pin.
3. Updates the ButtonState variable.
4. Other code can then use ButtonState to decide what to do.
void HandleButtonActions(){

  // Create a new landsacape after the button is released
  if(ButtonState == LOW && MenuState == false && CreateFlag == true && millis() - ButtonPressTime > 40){
    CreateNewLandscape();
    CreateFlag = false;
   } 

  // The menu settings are adjusted via the keyboard  
  // Pressing the button skips parameter selection and keeps the current values
  if(ButtonState == LOW && SkipFlag == false && MenuState == true){
    SkipFlag = true;
    Skipped = false;
  }

  // Button is pressed!
  if(ButtonState == HIGH){

    // Create landscape after the button is released is menu is off
    CreateFlag = true;

    // Start debounce filter
    if(ButtonPressTime ==  0) ButtonPressTime = millis();

    // Skip changes if the button is pressed
    if(MenuState == true && MenuKeyPressed < 2 && Skipped == false && millis() - ButtonPressTime > 40){
      /*...*/
      MenuKeyPressed++;
      /*...*/nalogWrite(LED_PIN, 0);
    } 

   // Open menu by holding the button
    if(millis() - ButtonPressTime >  1000 && !MenuState){
      MenuState = true;
      /*...*/
    }

  // When the button is not pressed
  }else{
    /*...*/
    CreateFlag = false;
  }

  if(MenuState == true){
    Menu();
  }

  // Save to EEPROM if settings were changed
  SaveToEEPOMMenuSettings();
}
This function takes the simple ButtonState (just "pressed" or "released") and decides what should actually happen based on that button press. It handles different types of button interactions: quick taps, long holds, and menu navigation.

Let's go through each section:
1. Creating a New Landscape (Short Press)
if(ButtonState == LOW && MenuState == false && CreateFlag == true && millis() - ButtonPressTime > 40){
CreateNewLandscape();
CreateFlag = false;
}

What it does: Creates a new random Landscape for the oscillator when you quickly press and release the button.
How it works: It only triggers when:
- Button is released (LOW)
- Not in menu mode (MenuState == false)
- We're allowed to create (CreateFlag == true)
- At least 40ms have passed (prevents accidental triggers)

2. Skipping Menu Changes
if(ButtonState == LOW && SkipFlag == false && MenuState == true){
SkipFlag = true;
Skipped = false;
}

- What it does: In menu mode, this lets you skip changing a parameter and keep the current value.

3. Button Pressed Logic (ButtonState == HIGH)
This big section handles what happens while the button is being held down:

Setup for Later
CreateFlag = true; // Remember to create landscape later
if(ButtonPressTime == 0) ButtonPressTime = millis(); // Start timing the press

- What it does: Records when the button press started and prepares for possible actions later.

Menu Navigation (Short Press in Menu)
if(MenuState == true && MenuKeyPressed < 2 && Skipped == false && millis() - ButtonPressTime > 40){
MenuKeyPressed++;
analogWrite(LED_PIN, 0);
}

- What it does: When you're in the menu, a short press selects or changes parameters.
- The millis() - ButtonPressTime > 40 is a debounce - it waits 40ms to make sure it's a real press, not electrical or mechanical noise.

Entering Menu Mode (Long Press)
if(millis() - ButtonPressTime > 1000 && !MenuState){
MenuState = true;
}

- What it does: If you hold the button for more than 1 second (1000ms), it enters menu mode.

4. Menu Management
if(MenuState == true){
Menu();
}

- What it does: If we're in menu mode, it calls the Menu() function to handle menu display and navigation.

6. Saving Settings
SaveToEEPOMMenuSettings();

Automatically saves any menu changes to permanent memory (EEPROM) so they're remembered after power-off.
// Change settings using the keyboard 
void Menu(void){
  // Change Element settings using the keyboard
  if(MenuKeyPressed == 1 && MenuUpdated == LOW){
    /*...*/
    SelectElementFunction = KeybordKey;
    /*...*/
  }
  // Change Scale settings using the keyboard
  if(MenuKeyPressed > 1){
    /*...*/
    ScaleNumber = KeybordKey;
    /*...*/
  }
}
Menu(): The Keyboard Control Center
This function lets you use a keyboard to change your synth's settings while you're in menu mode.

How It Works:
When you hold the button to enter menu mode, the synth waits for you to press keys on the keyboard to change settings. The MenuKeyPressed variable keeps track of which setting you're currently adjusting.
DAC.ino
void DAC(int Data, int CSpin, boolean Channel){
  Data |=0xf000;// B15(A/B)=1 B, B14(BUF)=1 on, B13(GAn) 1=x1  B12(SHDNn) 1=off
  if(Channel == true) Data &= ~0x8000; // for A-out
  SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0));
  digitalWrite(CSpin, LOW);
  SPI.transfer((0xff00 & Data)>>8);
  SPI.transfer(0x00ff & Data);
  digitalWrite(CSpin, HIGH);
  SPI.endTransaction();
}
Sends 12-bit values to MCP4922/MCP4921 DAC chips to generate analog voltages.

DAC Chips Used:
- MCP4922 - Dual channel (A and B outputs)
- MCP4921 - Single channel (A output only)

Function Parameters:
Data - 12-bit value to output (0-4095) (2048 = ~1.65V)
CSpin - Which DAC chip to talk to (DAC_ADSR_AND_DISTANCE_CS)
Channel - Which output: false = B, true = A

Configuration Bits
Setting Control Bits
Data |= 0xf000;

What this does: Sets the top 4 bits to control DAC behavior

Binary breakdown:
1111 0000 0000 0000
│││└─ B12: SHDN (Shutdown)
││└── B13: GA (Gain)
│└─── B14: BUF (Buffer)
└──── B15: A/B (Channel select)

SPI Transaction
1. Begin Transaction
SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0));

Settings:
- 20000000 - 20 MHz clock speed (fast!)
- MSBFIRST - Send Most Significant Bit first
- SPI_MODE0 - Clock polarity/phase (required by MCP49xx)

2. Select Chip
digitalWrite(CSpin, LOW);
Pull CS (Chip Select) pin LOW → "DAC, listen up!"

3. Send Data
SPI.transfer((0xff00 & Data)>>8); // Send high byte
SPI.transfer(0x00ff & Data); // Send low byte

Data format for MCP49xx: 16 bits total
Bits 15-12: Control bits (A/B, BUF, GA, SHDN)
Bits 11-0: 12-bit DAC value (0-4095)

4. Deselect Chip
digitalWrite(CSpin, HIGH);
Pull CS HIGH → "DAC, I'm done"
DAC latches the value and updates its output.

5. End Transaction
SPI.endTransaction();

Releases the SPI bus for other devices.
Distance.ino
void DistanceSensorInit(void){
  Sensor.setTimeout(500);
  Sensor.init();
  Sensor.setMeasurementTimingBudget(20000);
  Sensor.startContinuous();
}
What is VL53L0X?
VL53L0X - Time-of-Flight (ToF) distance sensor by STMicroelectronics

How it works:
- Shoots invisible infrared laser
- Measures time for light to bounce back
- Calculates distance (up to ~2 meters)

Musical application: Use hand gestures to control synthesizer parameters!

Initialization Steps
1. Set Timeout
Sensor.setTimeout(500);

What it does: Maximum time to wait for sensor response
Value: 500 milliseconds
Purpose: If sensor doesn't respond (disconnected, failed), don't freeze forever

2. Initialize Sensor
Sensor.init();

What it does:
- Powers up the sensor
- Loads calibration data
- Configures I2C communication
- Prepares sensor for measurements

Must be called before using the sensor!

3. Set Timing Budget
Sensor.setMeasurementTimingBudget(20000);

What it does: Sets how long each distance measurement takes.
Value: 20,000 microseconds = 20 milliseconds.
Why 20ms? Balance between responsiveness and accuracy for musical control.
Update rate: ~50 measurements per second - smooth for gesture control.

4. Start Continuous Mode
Sensor.startContinuous();

What it does: Sensor continuously measures distance without being asked
void ReadDistanceSensor(void){
  if(I2Cfree == true){
    I2Cfree = false;
    Wire1.beginTransmission(Address);
    Wire1.write(0x14 + 10); 
    Wire1.endTransmission();
    Wire1.requestFrom(0x29, 2);
    DistanceValue  = (uint16_t)Wire1.read() << 8;
    DistanceValue |= Wire1.read();
    I2Cfree = true;
  }
}
Reads the current distance measurement from the VL53L0X sensor via I2C.
Returns: DistanceValue in millimeters.

Step-by-Step Breakdown:
1. Check I2C Availability
if(I2Cfree == true)

Why needed? Prevents conflicts with MPR121 touch sensor (also uses I2C)

2. Start I2C Transaction
Wire1.beginTransmission(Address);

What it does: Begin communication with sensor
`Address`: VL53L0X I2C address (typically 0x29)

3. Request Distance Register
Wire1.write(0x14 + 10);

What it does: Tell sensor which register to read
Register address: 0x14 + 10 = 0x1E
0x1E: RESULT_RANGE_STATUS register - contains distance measurement
Why +10? Offset to access the specific result data bytes

4. End Write, Prepare Read
Wire1.endTransmission();

What it does: Finish writing the register address, keep connection open.

5. Request Data
Wire1.requestFrom(0x29, 2);

What it does: Ask sensor to send 2 bytes of distance data.

Parameters:
- 0x29 - Sensor I2C address
- 2 - Number of bytes to read

Why 2 bytes? Distance is a 16-bit value (0-65535)

6. Read High Byte
DistanceValue = (uint16_t)Wire1.read() << 8;

What it does: Read first byte (high/most significant byte) and shift left 8 bits.

7. Read Low Byte
DistanceValue |= Wire1.read();

What it does: Read second byte (low/least significant byte) and combine with high byte.

8. Release I2C Bus
I2Cfree = true;

Mark I2C as available for other devices.
void SmoothSensorData(void){
  DistanceValueBuffer[DistanceBufferCounter] = DistanceValue;
  DistanceBufferCounter++;
  if(DistanceBufferCounter > 4) DistanceBufferCounter = 0;
  SmoothedDistance = (DistanceValueBuffer[0] + DistanceValueBuffer[1] + DistanceValueBuffer[2] + DistanceValueBuffer[3] + DistanceValueBuffer[4]) / 5;
  if(SmoothedDistance > 350) SmoothedDistance = 350;
  if(SmoothedDistance < 50) SmoothedDistance = 50;
}
Smooths noisy distance sensor readings using a moving average filter.

Average the last 5 readings:
Reading 1: 150
Reading 2: 153
Reading 3: 148
Reading 4: 152
Reading 5: 149

Average: (150 + 153 + 148 + 152 + 149) / 5 = 150.4 mm ✓ Smooth!

How It Works:
1. Circular Buffer
DistanceValueBuffer[DistanceBufferCounter] = DistanceValue;

Store current reading in buffer array at current position.

2. Increment Counter
DistanceBufferCounter++;

Move to next position in buffer.

3. Wrap Around
if(DistanceBufferCounter > 4) DistanceBufferCounter = 0;

Reset to start when reaching the end → circular buffer.

4. Calculate Average
SmoothedDistance = (DistanceValueBuffer[0] + DistanceValueBuffer[1] +
DistanceValueBuffer[2] + DistanceValueBuffer[3] +
DistanceValueBuffer[4]) / 5;

Sum all 5 values and divide by 5 = moving average.

5. Constrain Range
if(SmoothedDistance > 350) SmoothedDistance = 350;
if(SmoothedDistance < 50) SmoothedDistance = 50;

Limit output to useful range:
- Minimum: 50mm (hand very close)
- Maximum: 350mm (comfortable gesture range)

Why? Beyond 350mm, readings become unreliable and less useful for control.

Why constrain to 50-350mm?
Too close (< 50mm):
- Unreliable readings
- Hand blocking sensor
- Not useful for control

Too far (> 350mm):
- Ambient light interference
- Less accurate
- Beyond comfortable gesture range
void DistanceUpdate(void){
  ReadDistanceSensor();
  SmoothSensorData();
  if(oldDistance != SmoothedDistance){
      /*...*/
      DAC(DACmapped, DAC_ADSR_AND_DISTANCE_CS, true);
      /*...*/
    }
    if(!Gate) analogWrite(LED_PIN, map(SmoothedDistance, 50, 350, 255, 0));
   } 
}
Complete distance sensor workflow: read → smooth → update DAC → update LED.
Purpose: Convert hand distance into control voltage and visual feedback.

Function Flow:

1. Read Sensor
ReadDistanceSensor();

Get current distance from VL53L0X (raw value).

2. Smooth Data
SmoothSensorData();

Apply moving average filter (removes jitter).

3. Check if Changed
if(oldDistance != SmoothedDistance){

Only update when distance actually changes.
Why? Avoid unnecessary SPI traffic and DAC updates.

4. Map to DAC Range
uint16_t DACmapped = map(SmoothedDistance, 50, 350, 0, 4095);

Convert distance to 12-bit DAC value.

5. Safe DAC Update
if(SPIfree == true && PIT_CVAL0 > 40) {
SPIfree = false;
DAC(DACmapped, DAC_ADSR_AND_DISTANCE_CS, true);
SPIfree = true;
oldDistance = SmoothedDistance;
}

Two safety checks before sending:

`SPIfree == true`
- SPI bus available?
- Don't interrupt audio DAC

`PIT_CVAL0 > 40`
- Timer has enough time?
- Don't conflict with audio sample timing

Then:
1. Mark SPI busy
2. Send value to DAC (channel A)
3. Mark SPI free
4. Remember last sent value

6. LED Feedback
if(!Gate) analogWrite(LED_PIN, map(SmoothedDistance, 50, 350, 255, 0));

Update LED brightness when no note playing.

Mapping:
50mm → 255 (hand close = bright LED)
200mm → 127 (middle = medium brightness)
350mm → 0 (hand far = dim LED)

Why `!Gate`? Only show distance when not playing - during notes, LED shows other info.
EEPROM.ino
void EEPROMreadSettings(void){
  for(int i = EEPROM_SIZE; i >= 0; i--){
    short data = EEPROM.read(i);
    if(data != 0){
      SelectElementFunction = data - 1;
      int temp = i-1;      
      ScaleNumber = EEPROM.read(temp) - 1;
      EEPROMaddress = i;
      break;
    } 
  }
}

void EEPROMwriteSettings(short ScaleNumber, short SelectElementFunction){
  /*...*/
  EEPROM.write(EEPROMaddress, SelectElementFunction);
  /*...*/
}

void EraseEEPROM(void){
  for(int i = 0; i <= EEPROM_SIZE;){
      EEPROM.write(i, 0);
      i++;
    }
    EEPROMaddress = 0;  
}
What is EEPROM?
EEPROM = Electrically Erasable Programmable Read-Only Memory
Key feature: Saves data even when power is off (non-volatile memory)
Use in Archean: Remember user settings between power cycles

What Gets Saved:

Two user preferences are stored:
1. ScaleNumber - Selected musical scale (Major, Minor, Pentatonic, etc.)
2. SelectElementFunction - What the "Element" parameter controls

Why save these? User's preferred settings persist after turning synth off/on.

EEPROM Write Strategy
Problem: EEPROM has limited write cycles (~100,000 writes per location)

Solution: Append-only writing
- Write to next available address each time
- Never overwrite same location repeatedly
- Extends EEPROM lifespan significantly

Last non-zero values = current settings

1. EEPROMreadSettings();
What It Does:
Scans EEPROM backward to find the most recent saved settings.

Step-by-Step:
1. Start from end:
for(int i = EEPROM_SIZE; i >= 0; i--)
Scan from highest address backward.

2. Read each location:
short data = EEPROM.read(i);

3. Find first non-zero:
if(data != 0)
Non-zero = settings exist here.

4. Read Element function:
SelectElementFunction = data - 1;
Why `-1`? Settings stored as 1-based, converted to 0-based index.

5. Read Scale (previous address):
int temp = i - 1;
ScaleNumber = EEPROM.read(temp) - 1;
Scale stored just before Element function.

6. Save current position:
EEPROMaddress = i;
break;
Remember where we found data, then exit loop.

2. EEPROMwriteSettings();
Saves new settings to the next available EEPROM addresses.

3. EraseEEPROM();
Clears all EEPROM memory when it fills up.

When Called?
EEPROM full: No more space to append settings.
Solution: Erase everything and start fresh.

How It Works:
1. Loop through all addresses:
for(int i = 0; i <= EEPROM_SIZE; )

2. Write zero to each:
EEPROM.write(i, 0);

3. Reset position:
EEPROMaddress = 0;

Result: Clean slate for new settings.
Element.ino
void ElementUpdate(void){ 
  switch(SelectElementFunction){
    case 0:
      /*...*/
      DigitalRandomNoise();
      /*...*/
      break;
    case 1:
      /*...*/
      break;
    case 2:
      /*...*/
      break;
    case 3:
      /*...*/
      break; 
    default: 
      /*...*/
  }
}

void DigitalRandomNoise(void){
  /*...*/
  DAC(random(0,4095), DAC_LFO_AND_ELEMENT_CS, false);
  /*...*/
}
What is the Element Block?
A multi-function module that can perform different roles in your synth patch.

Hardware:
- CV input (control voltage in)
- Knob (manual control)
- CV output (modulation out)

Software: Switchable functions controlled by SelectElementFunction

How It Works:
Switch statement selects which function to run based on user setting.

User changes this setting → Different Element behavior
Saved in EEPROM → Remembered after power off

Why Multiple Functions?
Element block = Swiss Army knife for different synthesis needs.

Example functions that could be implemented:
Case 0: Random noise (implemented)
Case 1: ADSR repeater
Case 2: Linar FM

One hardware block, many possibilities!

Why Use Switch?
Clear organization:
switch(SelectElementFunction) {
case 0: doFunction0(); break;
case 1: doFunction1(); break;
case 2: doFunction2(); break;
}

Advantages:
- Easy to add new functions
- Clear which function is active
- Efficient (compiler optimizes)
- Safe with default case
Keyboards.ino
Adafruit_MPR121 Left_MPR121 = Adafruit_MPR121();
Adafruit_MPR121 Right_MPR121 = Adafruit_MPR121();
What is MPR121?
MPR121 - Capacitive touch sensor chip by Freescale/NXP

Capabilities:
- Detects up to 12 touch inputs per chip
- Communicates via I2C
- Built-in noise filtering
- Adjustable sensitivity

Why Two MPR121 Chips?
One chip = 11 keys.
Two chips = 22 keys.

How it works:
- Conductive pads on PCB
- Human touch changes capacitance
- MPR121 detects the change
- Registers as key press

Creating MPR121 Objects
Adafruit_MPR121 Left_MPR121 = Adafruit_MPR121();

What this does:
- Creates an MPR121 controller object
- Provides functions to read touch status
- Handles I2C communication

Two separate objects = control two chips independently

Each MPR121 chip needs a unique address:
Left_MPR121.begin(0x5A); // First chip address
Right_MPR121.begin(0x5B); // Second chip address

MPR121 has two possible addresses:
- 0x5A - When ADDR pin tied to ground
- 0x5B - When ADDR pin tied to 3.3V

Hardware configuration sets address → Software knows which chip to read

Adafruit_MPR121 library provides:
- Simple initialization - begin() handles everything
- Easy reading - .touched() returns all keys at once
- Filtering - Built-in debounce and noise rejection
- Configuration - Adjustable sensitivity thresholds
- Reliable - Well-tested, widely used
void KeyboardRead(void){
  currtouched_left = Left_MPR121.touched();
  currtouched_right = Right_MPR121.touched();

  if(digitalRead(KEYBOARD_INTERRUPT_PIN) == HIGH){
    for(uint8_t i=0; i<11; i++){
      if((currtouched_left & _BV(i)) && !(lasttouched_left & _BV(i))){
        /*...*/
        KeybordKey = i;
        /*...*/
      }
    }
  }
  /*...*/
}
Detects which keyboard keys are pressed and identifies new key presses (not held keys).
Key feature: Only triggers on fresh touches, not continuous holds.

What .touched() Returns:

16-bit value where each bit represents one key:
Binary: 0000 0000 0000 1010
Key 3 Key 1 pressed.

Example:
currtouched_left = 0b0000000000001010;
Key 3 Key 1 are touched.

Step 2: Check Interrupt Pin
if(digitalRead(KEYBOARD_INTERRUPT_PIN) == HIGH){

Why Check Interrupt Pin?
MPR121 has hardware interrupt:
- Pulls pin LOW when no touch
- Pulls pin HIGH when any key touched

Purpose: Quick check if ANY keys are pressed before scanning all 12

Step 3: Scan Each Key
for(uint8_t i=0; i<11; i++){

Loop through keys 0-10 (11 keys total).
Why 11 and not 12? Implementation detail - might use 11 keys per chip instead of full 12.

Step 4: Detect New Key Press
if((currtouched_left & _BV(i)) && !(lasttouched_left & _BV(i))){

Two conditions (both must be true):
Condition 1: (currtouched_left & _BV(i))
- Is key i currently touched?
Condition 2: !(lasttouched_left & _BV(i))
- Was key i NOT touched last time?

Combined: Key is touched NOW but wasn't touched BEFORE = NEW press!

Understanding _BV(i)

`_BV(i)` = Bit Value macro = `(1 << i)`

Creates a bit mask for specific key:
_BV(0) = 0000 0000 0000 0001 (bit 0)
_BV(1) = 0000 0000 0000 0010 (bit 1)
_BV(2) = 0000 0000 0000 0100 (bit 2)
_BV(3) = 0000 0000 0000 1000 (bit 3)

Example:
_BV(3) = 1 << 3 = 0b0000000000001000

Step 5: Record Key Number
KeybordKey = i;

Store which key was pressed for further processing (trigger note, etc.)
LFO.ino
void LfoInterrupt(void){
  LFOCounter++;
  /*...*/
  if(LFOPointer > 1023) LFOPointer = 0;
  if (LFOWaveformSelector == true){
    LFOData = SINE[LFOPointer];
  }else{
    LFOData = PULSE[LFOPointer];
  }
  LFOCounter = 0;

  /*...*/
  DAC(LFOData, DAC_LFO_AND_ELEMENT_CS, true); 
  /*...*/
}
What is an LFO?
LFO = Low Frequency Oscillator
Purpose: Creates slow, repeating modulation (typically 0.1 - 20 Hz)

Musical uses:
- Vibrato - Pitch wobble
- Tremolo - Volume wobble
- Filter sweeps - Automatic wah-wah

Called by timer at regular intervals to generate smooth LFO waveform.
Output: Control voltage sent to DAC for modulation.

Step-by-Step Breakdown:
Step 1: Increment Counter
LFOCounter++;

Tracks position in the waveform cycle.
Counter increases each interrupt → Steps through wavetable.

Step 2: Wrap Pointer
if(LFOPointer > 1023) LFOPointer = 0;

Wavetables have 1024 samples (0-1023).
When reaching end, wrap to start → Continuous loop.
Position: 0 → 1 → 2 → ... → 1023 → 0 → 1 → 2 ...

Step 3: Select Waveform
Two Waveforms Available - Sine and Pulse.

Wavetables are pre-calculated arrays:
SINE[1024] = {2048, 2060, 2073, ...} // Smooth curve
PULSE[1024] = {0, 0, 0, ..., 4095, 4095, ...} // Square wave

`LFOPointer` indexes into array to get current value.

Step 4: Reset Counter
LFOCounter = 0;

Resets counter after reading wavetable.

Step 5: Send to DAC
DAC(LFOData, DAC_LFO_AND_ELEMENT_CS, true);

Output LFO value as analog voltage:
- Chip: LFO and Element DAC
- Channel: A (`true` = channel A)
- Value: Current waveform sample (0-4095)

Why pre-calculated waveforms?
- Fast - Array lookup = 2 CPU cycles
- Consistent - Perfect waveform shape every time
- Smooth - 1024 samples = high resolution
- Efficient - No real-time calculation needed
Landscape.ino
#define SIZE 256 // Size of the landscape array

uint16_t Landscape[SIZE]; // Main landscape height array
/*...*/

int MinValue;
int MaxValue;

int ReboundZoneMin = 30;   // Minimum height before rebound effect
int ReboundZoneMax = 190;  // Maximum height before rebound effect
int ReboundZoneStrength = 5;  // Strength of the rebound effect

int StartingHeight = 50;  // Initial terrain height

int Roughness = 5; // Controls terrain steepness (higher values create steeper slopes)

int CurrentHeight = StartingHeight;  // Current height of the terrain
int HeightIncrement = 0;  // Change in height for each step

extern short OSCsliderSelect;

// Generates three random landscapes for all oscillator modes at startup
void CreateLandsacapes(void){
  CreateLandscape(Landscape, SIZE);
  /*...*/
}

// Wrapper function for generating a new landscape.  
// Calls the parameterized function with predefined settings.  
// Used in the menu and other general contexts.
void CreateNewLandscape(void){
  CreateLandscape(Landscape, 256);
}

// Generates a new random landscape 
void CreateLandscape(uint16_t *Landscape, int size){
  for (int i = 1; i < size; i++) {
    // Adjust height based on rebound zones
    if (CurrentHeight <= ReboundZoneMin) {
        HeightIncrement += random(0, ReboundZoneStrength);
    }
    if (CurrentHeight >= ReboundZoneMax) {
        HeightIncrement -= random(0, ReboundZoneStrength);
    }

    // Apply random variation with respect to roughness
    HeightIncrement += random(-1 - round(HeightIncrement / Roughness), 2 - round(HeightIncrement / Roughness));
    CurrentHeight += HeightIncrement;

    // Store the generated height in the array
    Landscape[i] = CurrentHeight;
  }

  // Find the minimum and maximum height values
  MinValue = 300;
  MaxValue = 0;
  for (int i = 1; i < size; i++) {
    if (Landscape[i] > MaxValue) MaxValue = Landscape[i];
    if (Landscape[i] < MinValue) MinValue = Landscape[i];
  }

  // Normalize the height values to the 0-4095 range
  for (int i = 1; i < size; i++) {
    Landscape[i] = map(Landscape[i], MinValue, MaxValue, 0, 4095);
  }
}
What Are Landscape Waveforms?
Alternative to standard triangle wave - Random, evolving waveforms that look like mountain terrain.

Three modes:
1. Triangle Wave - Standard oscillator waveform
2. Static Landscape - Fixed random terrain
3. Morphing Landscapes - Continuously evolving terrain

Data Structures
#define SIZE 256

uint16_t Landscape[SIZE]; // Main landscape (mode 2)
uint16_t LandscapeA[SIZE]; // Morph source (mode 3)
uint16_t LandscapeB[SIZE]; // Morph target (mode 3)

256 samples per landscape = one waveform cycle.

Generation Parameters:
int ReboundZoneMin = 30; // Keep terrain from going too low
int ReboundZoneMax = 190; // Keep terrain from going too high
int ReboundZoneStrength = 5; // How strongly to push back
int StartingHeight = 50; // Where terrain starts
int Roughness = 5; // Terrain steepness (higher = steeper)

These create natural-looking terrain with peaks and valleys.

CreateLandscape() Function
Generates random terrain using a random walk algorithm.

1. Start at initial height:
CurrentHeight = StartingHeight; // Begin at 50

2. For each step (256 samples):

Check rebound zones:
if (CurrentHeight <= ReboundZoneMin) {
HeightIncrement += random(0, ReboundZoneStrength); // Push up
}
if (CurrentHeight >= ReboundZoneMax) {
HeightIncrement -= random(0, ReboundZoneStrength); // Push down
}
Prevents terrain from flying off or crashing.

3. Add random variation:
HeightIncrement += random(-1 - round(HeightIncrement / Roughness), 2 - round(HeightIncrement / Roughness));

Creates organic, unpredictable terrain.

4. Update height:
CurrentHeight += HeightIncrement;
Landscape[i] = CurrentHeight;

5. Normalize to DAC range (0-4095):
Landscape[i] = map(Landscape[i], MinValue, MaxValue, 0, 4095);
Scales terrain to full voltage range.

Why Use Landscapes?
- Unique sound - Different every time
- Evolving timbre - Constantly changing (morph mode)
- Experimental - Unexpected sonic results
- Creative - Generative music capabilities


MIDI.ino

void serialMIDIread(void){
  if (MIDI.read()){
    byte type = MIDI.getType();
    switch (type){
      case midi::NoteOn:
        /*...*/
        RecievedMIDINote = MIDI.getData1();
        MIDIvelocity = MIDI.getData2();
        Channel = MIDI.getChannel();
        /*...*/
      case midi::NoteOff:
        RecievedMIDINote = MIDI.getData1();
        Channel = MIDI.getChannel();
        /*...*/
      default:
        break;
    }
  }
}

/*...*/

void USBMIDISendMessages(void){
  /*...*/
}
MIDI Communication
serialMIDIread() Function

Reads incoming MIDI messages from external devices (keyboards, DAWs, sequencers).

Step-by-Step Breakdown:
1. Check for MIDI Message
if (MIDI.read()){

Returns `true` if new MIDI message available.
Non-blocking - Returns immediately if no message.

2. Get Message Type
byte type = MIDI.getType();

Identifies what kind of MIDI message arrived:
- Note On (key pressed)
- Note Off (key released)
- Control Change (knob/slider moved)
- Pitch Bend (bend wheel moved)
- etc.

3. Handle Note On
case midi::NoteOn:
RecievedMIDINote = MIDI.getData1();
MIDIvelocity = MIDI.getData2();
Channel = MIDI.getChannel();

Note On = Key pressed on MIDI keyboard

Three pieces of data:
getData1() - Note number (0-127):
60 = Middle C
61 = C#
72 = C one octave higher

getData2() - Velocity (0-127):
0 = Silent (often used as Note Off)
64 = Medium
127 = Maximum force

getChannel() - MIDI channel (1-16):
Which channel sent this message
Synth can filter by channel

4. Handle Note Off
case midi::NoteOff:
RecievedMIDINote = MIDI.getData1();
Channel = MIDI.getChannel();

Note Off = Key released

Two pieces of data:
- Note number (which key released)
- Channel (which channel)

No velocity - Release velocity rarely used.

USBMIDISendMessages() Function
Sends MIDI messages from Archean to external devices via USB.

Typical Implementation
void USBMIDISendMessages(void) {
// Send Note On when key pressed
usbMIDI.sendNoteOn(noteNumber, velocity, channel);

// Send Note Off when key released
usbMIDI.sendNoteOff(noteNumber, velocity, channel);

// Send Control Changes (knobs, sliders)
usbMIDI.sendControlChange(ccNumber, value, channel);
}

Communication Types
5-pin DIN MIDI connector:
- Standard MIDI cable
- Hardware synthesizers
- MIDI controllers

USB connection:
- Computer DAWs
- iOS/Android apps
- USB MIDI controllers
MIDIFreq.h
// Values are in microseconds
double midiNote[] = { 0,
/*...*/
238.89102706071438,
225.48310397275563,
212.82770978361953,
200.8826082916328,
/*
...
*/
}
MIDI Frequency Table
Lookup table converting MIDI note numbers to timer periods (in microseconds).
Purpose: Fast conversion from MIDI note to oscillator frequency.

MIDI Note System
MIDI notes numbered 0-127:
Note 0 = C-1 (8.18 Hz, sub-bass)
Note 60 = C4 (261.63 Hz, Middle C)
Note 69 = A4 (440 Hz, Concert A)
Note 127 = G9 (12,543.85 Hz, very high)

Each number = one semitone (half-step on piano).

Why Store Periods, Not Frequencies?
Teensy timer hardware needs periods (time per cycle), not frequencies.

The Relationship
Frequency = 1,000,000 / Period (in µs)
Period = 1,000,000 / Frequency

Example Conversions
Middle C (Note 60 = 261.63 Hz):
Period = 1,000,000 / 261.63 = 3,822.2 microseconds

A4 (Note 69 = 440 Hz):
Period = 1,000,000 / 440 = 2,272.7 microseconds

Higher notes → Higher frequency → Shorter period

Why Pre-Calculate?
Problems:
- Floating-point division is slow
- Delays while calculating = timing issues

With Lookup Table
double period = midiNote[note]; // Fast array access (2 cycles!)
timer.setPeriod(period);

Oscillator.ino
void OscillatorUpdate(void){
  noInterrupts();
  WaveTablePoint++;
  if(WaveTablePoint > 255) WaveTablePoint = 0;
  if(OSCsliderSelect == 0) OscValue = LandscapeA[WaveTablePoint];
  else if(OSCsliderSelect == 1) OscValue = Triangle[WaveTablePoint];
  else if(OSCsliderSelect == 2) OscValue = Landscape[WaveTablePoint];
  /*...*/
  DAC(OscValue, OSC_DAC_CS_PIN, true);
  /*...*/
  interrupts();
}
Generates audio samples at 44 kHz by reading wavetable values and sending them to the DAC.
This is the heart of sound generation!

How It Works:
1. Disable interrupts:
noInterrupts();

Critical section - No other interrupts can run during audio output.
Why? Prevent glitches, clicks, timing issues in audio.

2. Step through waveform:
WaveTablePoint++;
if(WaveTablePoint > 255) WaveTablePoint = 0;

Wavetable index loops 0 → 255 → 0
One complete cycle = one period of the waveform.

3. Select waveform:
if(OSCsliderSelect == 0) OscValue = LandscapeA[WaveTablePoint];
else if(OSCsliderSelect == 1) OscValue = Triangle[WaveTablePoint];
else if(OSCsliderSelect == 2) OscValue = Landscape[WaveTablePoint];

Three waveform options:
- 0: LandscapeA (morphing terrain)
- 1: Triangle (standard waveform)
- 2: Landscape (static terrain)

User selects waveform via slider or control.

4. Output to DAC:
DAC(OscValue, OSC_DAC_CS_PIN, true);

Send sample to oscillator DAC → Analog audio voltage output.

5. Allow interrupts:
interrupts();
void Pitch(void){
  /*...*/
  if(MIDInoteRecieved == false){
    /*...*/
    KeyFind = FindTone(KeybordNote, ScaleNumber);
    /*...*/
    
    //int OSCArrayIndex = ADC_Pitch_Filtered + int((KeyFind*12.2));
    int OSCArrayIndex = 0 + int((KeyFind*12.2));
    
    /*...*/
    OSC_Timer = V_OCT[OSCArrayIndex];
  }
  // If a MIDI note is received, sets frequency according to the MIDI pitch. 
  else{
    OSC_Timer = midiNote[RecievedMIDINote];
  }

  /*...*/

  // Updates the oscillator timer based on the calculated pitch frequency.
  OscillatorTimer.update(OSC_Timer);
}
Determines what frequency the oscillator plays based on multiple pitch sources.

Pitch Priority System
Two modes:

Mode 1: No MIDI Input (Manual Control)
Pitch determined by:
1. Touch keyboard - Which key pressed
2. 1V/OCT CV input - External voltage control
3. Tune knob - Coarse tuning
4. Fine knob - Fine tuning
5. Selected scale - Musical scale quantization

Formula:
KeyFind = FindTone(KeybordNote, ScaleNumber); // Quantize to scale
OSCArrayIndex = 0 + int((KeyFind*12.2)); // Calculate index
OSC_Timer = V_OCT[OSCArrayIndex]; // Look up period

Mode 2: MIDI Input Active
MIDI takes priority:
OSC_Timer = midiNote[RecievedMIDINote];

Simple and direct - MIDI note number → timer period.

Scale Quantization
FindTone() function:
KeyFind = FindTone(KeybordNote, ScaleNumber);

Snaps notes to selected musical scale.
Makes it impossible to play "wrong" notes!

Timer Update
OscillatorTimer.update(OSC_Timer);

Changes how fast OscillatorUpdate() is called:
Shorter period = faster calling = higher pitch
Longer period = slower calling = lower pitch
Scale.ino
int FindTone(short Key, short ScaleNumber){
  short Chrom[] = { 1 }; // 1 
  short Minor[] = { 2, 1, 2, 2, 1, 2, 2 }; // 2
  short Major[] = { 2, 2, 1, 2, 2, 2, 1 }; // 3
  /*...*/

  while(Counter != 0){
    switch(ScaleNumber){
      case 0:
        if (Step == 1) Step = 0;
        Scale = Chrom[Step];
        Tone = Tone + Scale;
        break;
      case 1:
        if (Step == 7) Step = 0;
        Scale = Minor[Step];
        Tone = Tone + Scale;
        break;
      /*...*/
      default: break;
    } 
    Step++;
    Counter--;
  } 
  return Tone;
}
Quantizes keyboard keys to selected musical scale - makes it impossible to play "wrong" notes!
Maps physical keys → scale notes → MIDI note numbers

FindTone() Function
Converts key number to the correct MIDI note based on selected scale.

Understanding Intervals
Semitones (Half-Steps)

One semitone = one key on piano (white or black)
Examples:
C → C# = 1 semitone
C → D = 2 semitones
C → E = 4 semitones

Chromatic "Scale"
short Chrom[] = { 1 };

Every semitone = all 12 notes available
No quantization - all keys play their natural pitch.

How the Algorithm Works
FindTone(Key, ScaleNumber);

Key: Which physical key pressed (0-21)
ScaleNumber: Which scale to use (0=Chromatic, 1=Minor, 2=Major, etc.)

Process:
1. Start at Tone = 0
2. For each key press (Counter = Key)
3. Look up interval in scale array
4. Add interval to Tone
5. Move to next scale step
6. Wrap around at end of scale pattern
7. Return total semitones
Wavetable.h
// Pulse for LFO
const int16_t PULSE[] = { 0, /*...*/ 4095 };

// Sine for LFO
const int16_t SINE[] = { 2048, /*...*/ 4095, /*...*/ 0, /*...*/ 2047 };

// Triangle for VCO
const int16_t Triangle[] = { 0, /*...*/ 4095, /*...*/  0 };
What Are Wavetables?
Pre-calculated waveform samples stored in arrays for fast audio generation.
Purpose: Instant waveform lookup instead of real-time calculation.

Data Type: int16_t
16-bit signed integer (-32,768 to +32,767)
Used here: 0 to 4095 (12-bit DAC range)

Why int16_t?
- Memory efficient (2 bytes per sample)
- Fast arithmetic
- Compatible with DAC range

const Keyword
const int16_t PULSE[] = {...};

Constant array - values never change after initialization.

Benefits:
- Stored in flash memory (not RAM)
- Can't be accidentally modified
- Compiler optimizations