Scale Practice Assistant
A digital metronome enhanced with looping, selectable scale playback for intentional, disciplined practice.
Components and supplies
1
Geekworm RAB Breadboard Base
1
Arduino Uno Rev3
2
Gikfun 5V Passive Piezo Buzzers
4
BOJACK 10Kohm Resistors
4
Chanzon 3mm LED (Clear / Transparent Lens)
1
Hosyond I2C 20x04 LCD Screen
1
BB830 Breadboard
1
ELEGOO Jumper Wires
6
BOJACK 100ohm Resistors
1
TUOFENG 22 AWG Solid Core Hookup Wire
2
ELEDIY 10K Potentiometers
4
DAOKI Tact Button Switches
Apps and platforms
1
Arduino IDE
Project description
Code
ScalePracticeBuddy.ino
cpp
Place 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}
Comments
Only logged in users can leave comments