QC

Scale Practice Assistant

An enhanced digital metronome with loop-selectable scale playback for intentional, disciplined practice.

Devices and components

Arduino Uno Rev3

Geekworm RAB Breadboard Base

Gikfun 5V passive piezoelectric buzzers

BOJACK 10Kohm Resistors

Chanzon 3mm LED (clear/transparent lens)

Hosyond I2C 20x04 LCD Screen

Breadboard BB830

ELEGOO connection wires

BOJACK 100ohm Resistors

TUOFENG 22 AWG Solid Core Jumper Wire

ELEDIY 10K potentiometers

DAOKI Touch Button Switches

Software and tools

Arduino IDE

Project description

Basic setup

Potentiometers

LEDs

Buttons

Piezoelectric buzzers

LCD

Finish

ScalePracticeBuddy.ino

Place it in a Sketch folder with the same name!

1// ******************** NOTE, SCALE, & SUBDIVISION DATA ********************* //
2/* --- Musical Notes ---
3 * These two parallel arrays hold the names and frequencies of 40 chromatic
4 * notes. We store data for 40 notes to cover every major and minor key on a
5 * standard 6-string guitar. We only use the note names for displaying the
6 * current key on the LCD, but this is a nice quality of life feature, and these
7 * note names can be useful if new features are added in the future.
8 */
9const int NUMBER_OF_NOTES = 40;
10const int NUMBER_OF_SCALE_ROOTS = 12; // For the 12 possible roots in an octave
11
12const char* NOTE_NAMES[NUMBER_OF_NOTES] = {
13 "E4", "F4", "Gb4", "G4", "Ab4", "A4", "Bb4", "B4", "C5", "Db5", "D5", "Eb5",
14 "E5", "F5", "Gb5", "G5", "Ab5", "A5", "Bb5", "B5", "C6", "Db6", "D6", "Eb6",
15 "E6", "F6", "Gb6", "G6", "Ab6", "A6", "Bb6", "B6", "C7", "Db7", "D7", "Eb7",
16 "E7", "F7", "Gb7", "G7"
17};
18
19const int NOTE_FREQUENCIES[NUMBER_OF_NOTES] = {
20 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622,
21 659, 698, 740, 784, 831, 880, 932, 988, 1047, 1109, 1175, 1245,
22 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 2349, 2489,
23 2637, 2794, 2960, 3136
24};
25
26/* --- Scale Patterns ---
27 * These two arrays hold the names and interval patterns for the major and minor
28 * scales. Additional scales can be added to the device by editing these arrays.
29 */
30const int NUMBER_OF_PATTERNS = 2;
31const char* SCALE_NAMES[NUMBER_OF_PATTERNS] = {"Major", "Minor"};
32const char* SCALE_PATTERNS[NUMBER_OF_PATTERNS] = {"WWHWWWH", "WHWWHWW"};
33
34/* --- Beat Subdivisions ---
35 * These two arrays hold the names and factors of three common subdivisions. The
36 * names are used for LCD output. The factors are used in calculating the length
37 * of one metronome event, which is crucial for synchronizing the scale playback
38 * with the metronome clicks.
39 */
40const int NUMBER_OF_FACTORS = 3;
41const char* SUB_NAMES[NUMBER_OF_FACTORS] = {
42 "Quarter Note", "Eighth Note ", "1/16 Note "
43};
44const int SUB_FACTORS[NUMBER_OF_FACTORS] = {
45 4, 8, 16
46};
47
48// ****************************** CUSTOM TYPES ****************************** //
49/* --- struct Scale ---
50 * This structure represents a musical scale. It has data members to store the
51 * scale name, interval pattern, and root index. It tracks its own interval
52 * location with an index variable. The actual scale is represented by an array
53 * of indices into the two note data arrays. Rather than a typical constructor,
54 * this struct uses function rebuild that takes a name, pattern, and root index
55 * to build a new scale at the same memory location. It also has a single getter
56 * to retrieve the frequency of the current note.
57 */
58struct Scale {
59 // Member Variables
60 static const int WRAPPED_SCALE_LENGTH = 17; // Number of notes when 'wrapped'
61 int notes[WRAPPED_SCALE_LENGTH]; // Indices to the note arrays
62 const int SCALE_LENGTH = 7; // Number of notes in the scale
63 const char* patternName = ""; // Name of the scale pattern
64 const char* patternSteps = ""; // Pattern of whole and half steps
65 int rootIndex = 0; // First note in the scale
66 int scaleIndex = 0; // Current note index in the scale
67
68 // Methods
69 /** rebuild()
70 * This function builds a scale from a root note, and intervals encoded in a
71 * pattern. After updating the member variables, the pattern is passed through
72 * a switch block one character at a time to derive the interval between each
73 * note. This interval is stored as an index into the note data arrays.
74 */
75 void rebuild(const char* name, const char* steps, int root) {
76 this -> patternName = name;
77 this -> patternSteps = steps;
78 this -> rootIndex = root;
79 this -> scaleIndex = 0;
80
81 // Start from this root value to build the scale from all chromatic notes
82 int scaleBuildingIndex = root;
83
84 // Variable to store the chromatic distance to the next note in the scale
85 int interval;
86
87 /* This for-loop builds the scale by parsing a scale interval from the
88 * patternSteps string, adding that interval to scaleBuildingIndex, and
89 * setting the next element in the notes array to the resulting new index.
90 * This program is equipped with major and minor scale patterns, but more
91 * can easily be added. If additional intervals are ever needed, they can be
92 * added to the switch block as new characters. For example, if we wanted to
93 * add a scale with minor or major third intervals, we could add 'm' to the
94 * switch to set interval = 3, and 'M' to set interval = 4.
95 */
96 for (int i = 0; i < WRAPPED_SCALE_LENGTH; i++) {
97 // Check if an index is out of the range of available chromatic notes
98 if (scaleBuildingIndex >= NUMBER_OF_NOTES) {
99 notes[i] = -1; // -1 indicates a non-existent note
100 continue;
101 }
102
103 // Store the current index in the notes array
104 notes[i] = scaleBuildingIndex;
105
106 // Parse the next interval from the patternSteps string
107 switch (patternSteps[i % SCALE_LENGTH]) {
108 case 'H': // Half step
109 interval = 1;
110 break;
111 case 'W': // Whole step
112 interval = 2;
113 break;
114 default: // Should not occur!
115 interval = 0;
116 break;
117 }
118
119 // Add the parsed interval to scaleBuildingIndex
120 scaleBuildingIndex += interval;
121 }
122 }
123
124 /* getCurrentNoteFrequency()
125 * Returns the frequency of the current note as an integer.
126 */
127 int getCurrentNoteFrequency() {
128 int index = notes[scaleIndex];
129
130 // If the note is invalid return 0, resulting in no tone being played
131 return (index != -1) ? NOTE_FREQUENCIES[index] : 0;
132 }
133};
134
135/* --- struct Button ---
136 * This structure encapsulates information about the past and current state of a
137 * physical button on the breadboard. This ended up being extremely important
138 * when trying to get the buttons to respond to a single press predictably and
139 * consistently. Due to the main loop in the Arduino sketch running so quickly,
140 * pressing a button would result in several -- sometimes dozens -- of
141 * individual presses being registered and handled by the code. It turns out
142 * that pressing a physical button can be a somewhat electrically messy process.
143 * The actual mechanism in the button will "bounce" several times in a matter of
144 * just a few milliseconds, creating a rapidly open and closed circuit to which
145 * our Arduino is surprisingly sensitive. In my search for a way to prevent this
146 * I came across a technique called "debouncing" where a change in the button
147 * state starts a timer, and the actual, stable button state isn't registered
148 * until enough time has passed since the state last changed. This duration is
149 * super quick -- 50 milliseconds in this case -- but adding that timer to the
150 * button handling GREATLY improves the button response. Packaging this data
151 * into a struct also allows us to use one function for checking any button,
152 * rather than having to repeat the debounce logic for each button individually.
153 *
154 * To that end, this struct contains the Arduino pin constant for a button, and
155 * varibales to store the last stable state of the button, the state of the pin
156 * during the last loop, and the time since the last state change.
157 */
158struct Button {
159 const int PIN; // Arduino pin
160 int lastState = LOW; // The last stable state
161 int lastPinState = LOW; // Input pin state in the last loop
162 unsigned long long lastDebounceTime = 0; // Time since the pin state changed
163
164 // Constructor
165 Button(int pin)
166 : PIN(pin) {}
167};
168
169/* --- struct Knob ---
170 * This structure encapsulates some basic information about a potentiometer.
171 * Similar to the Button struct above, we can monitor any number of
172 * potentiometers for change with a single function. The potentiometers are much
173 * easier to deal with than buttons, as we don't have to worry about any
174 * debounce logic. This struct has variables to store the Arduino pin constant,
175 * and the last value read, making it simple to detect changes.
176 */
177struct Knob {
178 const int PIN; // Arduino pin
179 int lastValue = 120; // Value read from the potentioment during the last loop
180
181 // Constructor
182 Knob(int pin)
183 : PIN(pin) {}
184};
185
186// ********************** DECLARATION & INITIALIZATION ********************** //
187// Includes
188#include <LiquidCrystal_I2C.h> // For LCD display
189#include <toneAC.h> // For second piezo
190#include <Wire.h> // Needed for LiquidCrystal_I2C.h
191
192// LCD Constants
193const int LCD_ADDRESS = 0x27;
194const int LCD_COLUMNS = 20;
195const int LCD_ROWS = 4;
196
197// LED Pins
198const int LED_1 = 6;
199const int LED_2 = 7;
200const int LED_3 = 11;
201const int LED_4 = 12;
202
203// Button Pins
204const int START_STOP = 2;
205const int TOGGLE_METRONOME = 3;
206const int CYCLE_SCALE = 4;
207const int CYCLE_KEY = 5;
208
209// Constant debounce delay for button functionality
210const int DEBOUNCE_DELAY = 50;
211
212// Potentiometer Pins
213const int TEMPO_POT = A0;
214const int SUBDIVISION_POT = A1;
215
216// Piezo Pin
217const int MELODY_SPEAKER = 8;
218
219// Metronome Click Frequencies
220const int ACCENT_CLICK = 4450;
221const int CLICK = 3900;
222
223// Variables -- Tempo
224int const DEFAULT_TEMPO = 120; // Default to 120 BPM tempo
225int tempo = DEFAULT_TEMPO; // Set default tempo
226int beatDuration; // Quarter note beat in milliseconds
227
228// Variables -- Timing
229int const DEFAULT_SUBDIVISION = 2; // Default to quarter note subdivision
230int subdivisionIndex = DEFAULT_SUBDIVISION; // Index into subdivision arrays
231int subdivisionFactor = SUB_FACTORS[DEFAULT_SUBDIVISION]; // Set default factor
232int subdivisionDuration; // Subdivision duration in milliseconds
233
234// Variables -- Metronome & Scale
235int count = 1; // Begin metronome count on 1
236int clickSound; // Stores the metronome click frequency
237int scaleRootIndex = 0; // Index for the starting notes of the current scale
238int scaleDataIndex = 0; // Index into scale data arrays (names and patterns)
239int scaleDirection = 1; // 1 == ascending scale, -1 == descending scale
240
241// Variables -- Device Control
242bool running = false; // Device starts in an idle state
243bool metronomeIsOn = true; // Toggleable metronome is on by default
244
245// Variables -- Non-Blocking Code
246unsigned long long previousEventMillis = 0; // Timestamp of the last event
247int eventDuration; // Interval between events based
248
249// Create an LCD object
250LiquidCrystal_I2C lcd(LCD_ADDRESS, LCD_COLUMNS, LCD_ROWS);
251
252// Create an empty Scale object
253Scale scale;
254
255// Create Button objects
256Button startButton(START_STOP);
257Button metronomeButton(TOGGLE_METRONOME);
258Button scaleButton(CYCLE_SCALE);
259Button keyButton(CYCLE_KEY);
260
261// Create Knob objects
262Knob tempoKnob(TEMPO_POT);
263Knob subdivisionKnob(SUBDIVISION_POT);
264
265// ****************************** SKETCH SETUP ****************************** //
266void setup() {
267 Serial.begin(9600); // Serial monitor output
268
269 // Set up Arduino pins
270 pinMode(START_STOP, INPUT);
271 pinMode(TOGGLE_METRONOME, INPUT);
272 pinMode(CYCLE_SCALE, INPUT);
273 pinMode(CYCLE_KEY, INPUT);
274 pinMode(LED_1, OUTPUT);
275 pinMode(LED_2, OUTPUT);
276 pinMode(LED_3, OUTPUT);
277 pinMode(LED_4, OUTPUT);
278 pinMode(MELODY_SPEAKER, OUTPUT);
279
280 // Initialize LCD
281 lcd.init();
282 lcd.backlight();
283
284 // Show the start-up 'splash'
285 showStartupDisplay();
286
287 // Initialize tempo and subdivision
288 updateTempo();
289 updateSubdivision();
290
291 // Build a default E Major scale
292 scale.rebuild(SCALE_NAMES[scaleDataIndex],
293 SCALE_PATTERNS[scaleDataIndex],
294 scaleRootIndex);
295
296 // Show the static display layout
297 delay(3000); // Delay to keep the start-up splash displayed
298 showStaticRunningDisplay();
299}
300
301// ****************************** SKETCH LOOP ******************************* //
302void loop() {
303 // Real-time Operations -- Occur in each iteration of the main loop
304 handleKnobs(); // Check potentiometer states
305 handleButtons(); // Check button states
306 updateDisplay(); // Update the LCD
307
308 // Timing Operations -- Occur at expected and controllable intervals
309 if (running) { // Is the device running?
310 if (nextEvent()) { // Check if the eventDuration has lapsed
311 click(); // Trigger metronome click logic
312 playNextNote(); // Trigger scale playback logic
313 }
314 }
315}
316
317// ********************** METRONOME & SCALE FUNCTIONS *********************** //
318/** click()
319 * This function controls the active musical count, plays metronome clicks on
320 * the quarter notes, and calls for the LEDs to update based on the count.
321 */
322void click() {
323 // Select click sound -- accent on a count of 1
324 if (count == 1) {
325 clickSound = ACCENT_CLICK;
326 } else {
327 clickSound = CLICK;
328 }
329
330 /* Produce click sounds on each quarter note beat. Many eventDuration values
331 * will cause click() to be called in between quarter note beats, so we first
332 * need to check if the current call falls on a beat. If we are between beats,
333 * the count will increment but no tone will be produced by the piezo. The
334 * maximum value count can hold is determined by the subdivision factor.
335 */
336 if (count % (subdivisionFactor / 4) == 1 || subdivisionFactor == 4) {
337 if (metronomeIsOn) { // Produce a click if the toggleable metronome is on
338 /* Call to toneAC takes a frequency, volume (10 is MAX), duration, and a
339 * boolean for automatically stopping after the duration has elapsed.
340 */
341 toneAC(clickSound, 10, 50, true);
342 }
343 updateLEDs();
344 }
345 count++;
346 // Check if the count needs to be reset to 1
347 if (count > subdivisionFactor && count > 4) {
348 count = 1;
349 }
350}
351
352/** updateLEDs()
353 * Controls the LED metronome indicators. The functions starts by turning all
354 * LEDs off, and then activates the appropriate LED based on the current count.
355 */
356void updateLEDs() {
357 // Turn off all LEDs
358 digitalWrite(LED_1, LOW);
359 digitalWrite(LED_2, LOW);
360 digitalWrite(LED_3, LOW);
361 digitalWrite(LED_4, LOW);
362 if (count == 1) { // First beat
363 digitalWrite(LED_1, HIGH);
364 } else if (count == 1 + (subdivisionFactor / 4)) {
365 digitalWrite(LED_2, HIGH); // Second beat
366 } else if (count == 1 + 2 * (subdivisionFactor / 4)) {
367 digitalWrite(LED_3, HIGH); // Third beat
368 } else if (count == 1 + 3 * (subdivisionFactor / 4)) {
369 digitalWrite(LED_4, HIGH); // Fourth beat
370 }
371}
372
373/** playNextNote()
374 * This function controls playing individual notes from the current scale. It
375 * is also responsible for controlling the ascending or descending direction of
376 * the scale, and updating the scaleIndex variable.
377 */
378void playNextNote() {
379 int frequency = scale.getCurrentNoteFrequency();
380 tone(MELODY_SPEAKER, frequency, eventDuration - 1);
381
382 if (scaleDirection == 1 && scale.scaleIndex == scale.WRAPPED_SCALE_LENGTH - 1)
383 {
384 scaleDirection = -1;
385 } else if (scaleDirection == -1 && scale.scaleIndex == 0) {
386 scaleDirection = 1;
387 }
388 scale.scaleIndex += scaleDirection;
389}
390
391// *************************** TIMING FUNCTIONS ***************************** //
392/** nextEvent()
393 * This function checks the duration of the current event against a new
394 * millisecond reading and returns true if the event duration has elapsed to
395 * begin the next event.
396 */
397bool nextEvent() {
398 // Capture current running time
399 unsigned long long currentEventMillis = millis();
400
401 // Check current running time against the previous event timestamp
402 if (currentEventMillis - previousEventMillis >= eventDuration) {
403 previousEventMillis = currentEventMillis;
404 return true; // time for the next event!
405 }
406 return false; // Not enough time has passed, the previous event still active
407}
408
409// ************************** DISPLAY FUNCTIONS ***************************** //
410/** showStartupDisplay()
411 * Outputs a startup splash to the LCD.
412 */
413void showStartupDisplay() {
414 lcd.setCursor(0, 0); lcd.print("--------------------");
415 lcd.setCursor(0, 1); lcd.print("Scale Practice Buddy");
416 lcd.setCursor(0, 2); lcd.print(" Version 1.00 ");
417 lcd.setCursor(0, 3); lcd.print("--------------------");
418}
419
420/** showStaticRunningDisplay()
421 * Outputs the static HUD elements to the LCD.
422 */
423void showStaticRunningDisplay() {
424 lcd.setCursor(0, 0); lcd.print("TEMPO : ");
425 lcd.setCursor(0, 1); lcd.print("METRO : ");
426 lcd.setCursor(0, 2); lcd.print("SCALE : ");
427 lcd.setCursor(0, 3); lcd.print("SUB : ");
428}
429
430/** updateDisplay()
431 * Outputs dynamic data points to the LCD.
432 */
433void updateDisplay() {
434 // Print the current tempo
435 lcd.setCursor(8, 0);
436 lcd.print(tempo);
437 lcd.print(" BPM ");
438
439 // Print the metronome state
440 lcd.setCursor(8, 1);
441 if (metronomeIsOn) {
442 lcd.print("ON ");
443 } else {
444 lcd.print("OFF");
445 }
446
447 // Print the current key and scale
448 lcd.setCursor(8, 2);
449 const char* key = NOTE_NAMES[scale.rootIndex]; // Get note name from array
450 for (int i = 0; key[i] != '\0'; i++) { // Loop through the C-string
451 char c = key[i]; // Store the current character
452 if (isDigit(c)) { // Print characters (no digits)
453 break; // Skip digits
454 }
455 lcd.print(c);
456 }
457 lcd.print(" ");
458 lcd.print(scale.patternName); // Print the name of the scale
459 lcd.print(" ");
460
461 // Print current subdivision
462 lcd.setCursor(8, 3);
463 lcd.print(SUB_NAMES[subdivisionIndex]);
464}
465
466// ******************** POTENTIONMETER STATE FUNCTIONS ********************** //
467/** handleKnobs()
468 * This function passes each potentiometer object to knobIsTurned() to detect
469 * changes. If a potentiometer state has changed update the applicable property.
470 */
471void handleKnobs() {
472 if (knobIsTurned(tempoKnob)) { // Check tempo potentiometer
473 updateTempo(); // Update tempo when a change is detected
474 }
475 if (knobIsTurned(subdivisionKnob)) { // Check subdivision potentiometer
476 updateSubdivision(); // Change is detected, update subdivision
477 }
478}
479
480/** updateTempo()
481 * This function updates the tempo and calculates new beat and event durations.
482 */
483void updateTempo() {
484 // Map the last potentiometer state to a valid tempo
485 int newTempo = map(tempoKnob.lastValue, 0, 1023, 40, 240);
486
487 // Check if the mapped tempo value is different from the current tempo
488 if (newTempo != tempo) {
489
490 // Store the new tempo value
491 tempo = newTempo;
492
493 // Calculate a new beatDuration -- 60000 milliseconds in one minute
494 beatDuration = 60000 / tempo;
495
496 // Calculate a new event duraction
497 eventDuration = beatDuration / (subdivisionFactor / 4);
498 }
499}
500
501/** updateSubdivision()
502 * Function updates the subdivision factor and recalculates the event duration.
503 */
504void updateSubdivision() {
505 // Map the last potentiometer state to a valid subdivision index
506 int newSubdivisionIndex = map(subdivisionKnob.lastValue, 0, 1023, 0, 3);
507
508 // Check if the mapped index is different from the current subdivision index
509 if (newSubdivisionIndex != subdivisionIndex) {
510
511 // Store the new subdivision index
512 subdivisionIndex = newSubdivisionIndex;
513
514 // Get the new subdivision factor from the array
515 subdivisionFactor = SUB_FACTORS[subdivisionIndex];
516
517 // Calculate a new event duration
518 eventDuration = beatDuration / (subdivisionFactor / 4);
519 }
520}
521
522/** knobIsTurned()
523 * This function checks the state of a given potentiometer and returns true if a
524 * change in value is detected. The difference between the current and previous
525 * values must be greater than 5 to register as a change. This is to create
526 * smoother and more stable potentiometer behavior. Without this small buffer,
527 * the potentiometers will tend to vary by tiny amounts in each loop, causing
528 * stability problems in the device.
529 */
530bool knobIsTurned(Knob& k) {
531
532 // Create a variable to store the result
533 bool turned = false;
534
535 // Read the value from the Arduino pin
536 int reading = analogRead(k.PIN);
537
538 // Check if the value is different from the last recorded value
539 if (abs(reading - k.lastValue) > 5) {
540 turned = true; // Change detected!
541 k.lastValue = reading; // Update the last recorded value
542 }
543 return turned;
544}
545
546// ************************ BUTTON STATE FUNCTIONS ************************** //
547/** handleButtons()
548 * This function passes each button object to buttonIsPressed() to detect button
549 * presses. When a press is detected, the pertinent device state is updated.
550 */
551void handleButtons() {
552 // Check start/stop button
553 if (buttonIsPressed(startButton)) {
554 running = !running; // Toggle running state
555 if (!running) { // If stopping the device
556 noTone(MELODY_SPEAKER); // Stop any tone the piezo is producing
557 count = 0; // Causes updateLEDs() to turn all LEDs off
558 updateLEDs(); // Turn off all LEDs
559 resetDevice(); // Revert to starting state
560 }
561 }
562
563 // Check metronome toggle button
564 if (buttonIsPressed(metronomeButton)) {
565 metronomeIsOn = !metronomeIsOn; // Toggle audible metronome clicks
566 }
567
568 // Check scale cycle button
569 if (buttonIsPressed(scaleButton)) {
570
571 // Cycle to the next scale pattern. Starts with 2, but more can be added!
572 scaleDataIndex = (scaleDataIndex + 1) % NUMBER_OF_PATTERNS;
573
574 // Build a new scale with the selected pattern
575 scale.rebuild(SCALE_NAMES[scaleDataIndex],
576 SCALE_PATTERNS[scaleDataIndex],
577 scaleRootIndex);
578 resetDevice(); // count = 1, scaleIndex = 0, update tempo and subdivision
579 }
580
581 // Check key cycle button
582 if (buttonIsPressed(keyButton)) {
583
584 // Cycle to the next key. Any of the 12 root notes can be selected!
585 scaleRootIndex = (scaleRootIndex + 1) % NUMBER_OF_SCALE_ROOTS;
586
587 // Build a new scale with the selected key
588 scale.rebuild(SCALE_NAMES[scaleDataIndex],
589 SCALE_PATTERNS[scaleDataIndex],
590 scaleRootIndex);
591 resetDevice(); // count = 1, scaleIndex = 0, update tempo and subdivision
592 }
593}
594
595/** buttonIsPressed()
596 * This function checks the state of a single button and returns true if a
597 * change is detected after the state is put through the debouncing logic.
598 */
599bool buttonIsPressed(Button& b) {
600
601 // Create a variable to store the result
602 bool pressed = false;
603
604 // Read the state of the Arduino pin
605 int currentReading = digitalRead(b.PIN);
606
607 // Check if the reading is different from the pin state in the last loop
608 if (currentReading != b.lastPinState) { // Change detected!
609 b.lastDebounceTime = millis(); // Set the debounce timer
610 }
611
612 // Check if the debounce delay has elapsed, signifying a stable button state
613 if ((millis() - b.lastDebounceTime) > DEBOUNCE_DELAY) {
614
615 // Check if the reading is different from the last stable button state
616 if (currentReading != b.lastState) { // Change detected!
617
618 // Update the last stable state
619 b.lastState = currentReading;
620
621 // Check for a HIGH value, indicating a button press
622 if (b.lastState == HIGH) {
623 pressed = true;
624 }
625 }
626 }
627
628 // Update the last unstable pin state
629 b.lastPinState = currentReading;
630 return pressed;
631}
632
633/** resetDevice()
634 * Reset the device to a starting state.
635 */
636void resetDevice() {
637 count = 1; // Reset count
638 scale.scaleIndex = 0; // Reset scale index
639 updateTempo(); // Set the current tempo
640 updateSubdivision(); // Set the current subdivision
641}




Note: Content and images are from: https://projecthub.arduino.cc/, with some modifications.
If you want it removed due to copyright reasons, please leave a comment. Thank you.
I want to share this article more widely so that everyone knows about Arduino and your project.

Arduino Tutorial: Mini Piano

In this video I show you how to make a mini piano with Arduino. Devices and components Arduino Uno Rev3 Jumper wires (generic) Buzzer Breadb...