Audio Workshop 4
Do More While Playing Music
In the previous tutorial, we saw you can use delay() without disrupting the audio library, but it does prevent your program from rapidly responding to user actions such as turning a volume control knob.
In this part, we’ll look at how we can blink the LED without using delay(), so that our sketch remains responsive to user input.
We will also look into the details of reading the knobs and pushbuttons and do more things while music plays. We will also mention some of the finer details of the Arduino sketch example code.
To get started, download the following program and then we will go over some of the details.
Complete Program
// Advanced Microcontroller-based Audio Workshop // // http://www.pjrc.com/store/audio_tutorial_kit.html // https://hackaday.io/project/8292-microcontroller-audio-workshop-had-supercon-2015 // // Part 1-5: Respond to Pushbuttons & Volume Knob // // Do more while playing. Monitor pushbuttons and adjust // the volume. Whe the buttons are pressed, stop playing // the current file and skip to the next or previous. // // ProtoSupplies.com Changes and Additions: // Change pin #s to match Tutorial Adapter to avoid conflicts // Use VOL potentiometer on Tutorial Adapter for volume // Add Touch LCD support // Show WAV file playing on LCD #include <ILI9341_t3.h> #include <font_Arial.h> #include <font_ArialBold.h> #include <XPT2046_Touchscreen.h> #include <Audio.h> #include <Wire.h> #include <SPI.h> #include <SD.h> #include <SerialFlash.h> #include <Bounce.h> AudioPlaySdWav playSdWav1; AudioOutputI2S i2s1; AudioConnection patchCord1(playSdWav1, 0, i2s1, 0); AudioConnection patchCord2(playSdWav1, 1, i2s1, 1); AudioControlSGTL5000 sgtl5000_1; Bounce button0 = Bounce(3, 15); Bounce button2 = Bounce(5, 15); // 15 = 15 ms debounce time // Use these with the Teensy Audio Shield #define SDCARD_CS_PIN 10 #define SDCARD_MOSI_PIN 7 #define SDCARD_SCK_PIN 14 // Use these with the Teensy 4.1 SD card //#define SDCARD_CS_PIN BUILTIN_SDCARD //#define SDCARD_MOSI_PIN 11 // not actually used //#define SDCARD_SCK_PIN 13 // not actually used // touchscreen offset for four corners #define TS_MINX 400 #define TS_MINY 400 #define TS_MAXX 3879 #define TS_MAXY 3843 // LCD control pins defined by board #define TFT_CS 40 #define TFT_DC 9 // Use main SPI bus MOSI=11, MISO=12, SCK=13 with different control pins ILI9341_t3 tft = ILI9341_t3(TFT_CS, TFT_DC); // Touch screen control pins defined by board // TIRQ interrupt if used is on pin 2 #define TS_CS 41 //#define TIRQ_PIN 2 XPT2046_Touchscreen ts(TS_CS); // Param 2 = NULL - No interrupts //=============================================================================== // Initialization //=============================================================================== void setup() { Serial.begin(9600); // Setup LCD screen tft.begin(); tft.setRotation(3); ts.begin(); ts.setRotation(1); tft.fillScreen(ILI9341_DARKGREEN); tft.setFont(Arial_10); tft.setCursor(20, 220); tft.print("Start Playing: "); tft.setFont(Arial_10_Bold); // Setup Audio Adapter AudioMemory(8); sgtl5000_1.enable(); sgtl5000_1.volume(0.5); SPI.setMOSI(SDCARD_MOSI_PIN); SPI.setSCK(SDCARD_SCK_PIN); if (!(SD.begin(SDCARD_CS_PIN))) { while (1) { Serial.println("Unable to access the SD card"); delay(500); } } pinMode(24, OUTPUT); // LED on pin 24 pinMode(3, INPUT_PULLUP); // Left button pinMode(5, INPUT_PULLUP); // Right button delay(1000); } int filenumber = 0; // while file to play const char * filelist[4] = { "SDTEST1.WAV", "SDTEST2.WAV", "SDTEST3.WAV", "SDTEST4.WAV" }; elapsedMillis blinkTime; //=============================================================================== // Main //=============================================================================== void loop() { if (playSdWav1.isPlaying() == false) { const char *filename = filelist[filenumber]; filenumber = filenumber + 1; if (filenumber >= 4) filenumber = 0; Serial.print("Start playing "); Serial.println(filename); tft.fillRect(120, 220, 100, 15, ILI9341_DARKGREEN); // Clear previous value tft.setCursor(120, 220); tft.print(filename); playSdWav1.play(filename); delay(10); // wait for library to parse WAV info } // blink the LED without delays if (blinkTime < 250) { digitalWrite(24, LOW); } else if (blinkTime < 500) { digitalWrite(24, HIGH); } else { blinkTime = 0; // start blink cycle over again } // read pushbuttons button0.update(); if (button0.fallingEdge()) { playSdWav1.stop(); } button2.update(); if (button2.fallingEdge()) { playSdWav1.stop(); filenumber = filenumber - 2; if (filenumber < 0) filenumber = filenumber + 4; } // read the VOL knob position (analog input A1) int knob = analogRead(A1); float vol = (float)knob / 1280.0; sgtl5000_1.volume(vol); //Serial.print("volume = "); //Serial.println(vol); }
Once the program is running, you should still see the LED blink. The VOL knob now controls the volume immediately regardless of where the LED is within its blinking cycle. The left and right pushbuttons now allow you to cycle UP or DOWN through the 4 songs on the SD card.
Removing the Delay() Delay
To remove the pause caused by delay(), the blinking without delay section of code uses a special elapsedMillis variable. It acts like a normal 32-bit integer, but it automatically increments 1000 times per second (every millisecond) with no help from us.
elapsedMillis blinkTime;
// blink the LED without delays if (blinkTime < 250) { digitalWrite(24, LOW); } else if (blinkTime < 500) { digitalWrite(24, HIGH); } else { blinkTime = 0; // start blink cycle over again }
This makes blinking without delay very simple. As we loop through the program, if the blinkTime variable is still under 250, the LED is set to be OFF. If it’s incremented to between 250 to 500, the LED is set to be ON. If it’s gone past 500, the variable is simply set back to zero to restart the blink cycle.
Read and Debounce the Pushbuttons
The pushbuttons are read using the Bounce library.
While you could use Arduino’s digitalRead() function to directly read the pins, Bounce automatically handles mechanical chatter in the pushbutton which is called ‘debouncing’.
This chatter occurs when the mechanical contacts inside the switch bounce several times before finally setting down when the pushbutton is pressed or released as shown in the O’scope capture.
Without Bounce, this can look like multiple button presses and cause erratic behavior of the program. Bounce does this without requiring a delay which could slow our response to other user input.
The Bounce library is easy to use, but it does require a few small pieces of code. First, a Bounce object is created for each pushbutton. The digital pin number the pushbutton is on and worst-case mechanical chatter time are given in milliseconds. Since you won’t know what the worst-case mechanical chatter time is unless you have an O’scope, just use the rule that if multiple button presses are detected when there should only be one, try increasing the debounce time by about 5 milliseconds at a time.
In this case button 0 is on digital pin 3 and has a 15 millisecond debounce time and button 2 is on digital pin 5 also with a 15 millisecond debounce time.
Bounce button0 = Bounce(3, 15); Bounce button2 = Bounce(5, 15); // 15 = 15 millisecond debounce time
Inside setup(), the digital pins need to be configured with INPUT_PULLUP mode. The pushbuttons connect the pin to ground (LOW) when pressed, so the pullup causes the pin to be pulled HIGH when the pushbutton is not pressed.
pinMode(24, OUTPUT); // LED on pin 24 pinMode(3, INPUT_PULLUP); // Left button pinMode(5, INPUT_PULLUP); // Right button delay(1000);
Inside loop(), the pushbuttons are checked with update() and fallingEdge(). Because the pushbutton connects to ground when pressed, this is called falling edge as the signal falls from a HIGH to LOW. When the button is released and rises from LOW to a HIGH due to the pullup, this is called rising edge. The Bounce library allows us to reliably detect the falling and rising edge, even if the button has chatter, and without ever delaying our sketch.
// read pushbuttons button0.update(); if (button0.fallingEdge()) { playSdWav1.stop(); } button2.update(); if (button2.fallingEdge()) { playSdWav1.stop(); filenumber = filenumber - 2; if (filenumber < 0) filenumber = filenumber + 4; }
All the examples in this tutorial use Bounce to read the pushbuttons and it is a very useful tool to keep in mind for other programs that require button input.
Colorizing the Display
The LCD background is now green and the information of which song is playing has been moved to the bottom of the screen and the file name is now shown in bold font.
// Setup LCD screen tft.begin(); tft.setRotation(3); ts.begin(); ts.setRotation(1); tft.fillScreen(ILI9341_DARKGREEN); tft.setFont(Arial_10); tft.setCursor(20, 220); tft.print("Start Playing: "); tft.setFont(Arial_10_Bold);
The color of the screen can be set to virtually any color, but is most easily changed by using one of the predefined colors in the IlI9341_t3.h library which include the following constants. You can try a few to see the effect.
ILI9341_BLACK, ILI9341_NAVY, ILI9341_DARKGREEN, ILI9341_DARKCYAN, ILI9341_MAROON, ILI9341_PURPLE, ILI9341_OLIVE, ILI9341_LIGHTGREY, ILI9341_DARKGREY, ILI9341_BLUE, ILI9341_GREEN, ILI9341_CYAN, ILI9341_RED, ILI9341_MAGENTA, ILI9341_YELLOW, ILI9341_WHITE, ILI9341_ORANGE, ILI9341_GREENYELLOW, ILI9341_ PINK
Controlling the Audio Volume
The final piece of code, which we will see repeated throughout the workshop, reads the VOL knob.
// read the VOL knob position (analog input A1) int knob = analogRead(A1); float vol = (float)knob / 1280.0; sgtl5000_1.volume(vol); //Serial.print("volume = "); //Serial.println(vol);
The Teensy 4.1 has 10-bit ADC’s, so the Arduino analogRead() function gives an integer value between 0 to 1023. Most audio library functions need a floating point number between 0 to 1.0, so throughout the workshop you will see equations that convert the 0-1023 integer to floating point.
In this case, a volume more than 0.8 is too loud, so a divisor of 1280.0 is used. Each example varies slightly, but these basic concepts are used throughout the example code in the workshop.