It’s quite a large app, with some of my own libraries included that aren’t in the Library Manager, but here goes:
/*
Shell House Jukebox
A proof of concept prototype for an audio jukebox player as an exhibit
in the restored ASUW Shell House.
-- Andrew Davidson
-- adavid7@uw.edu
CHANGE LOG
----------------------------------------------------------------------
0.4 first working version
reading metadata file for album clips
playing them on the VS1053 board
0.5a assorted enhancements
add volume control & NeoPixel strip display of volume
make debugging messages conditional
add next/prev clip buttons
add separate play/pause button
add autoplay mode and controls
rearrange UI hardware & code for OLED, buttons, NeoPixel strips
0.6 add OLED display, assorted bug fixes
change button functions
display status and other info on OLED
0.6a attempt to fix OLED update lagging
0.7 switch to Arduino GIGA
*/
/*
UI operations summary
‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡
‡ ‡ | | | | | ‡
‡ ‡ VOLUME | SELECT | PLAY/PAUSE | PREVIOUS | NEXT | SYSTEM ‡
‡ ‡ ENCODER | ENCODER | BUTTON | BUTTON | BUTTON | BUTTON ‡
‡ ‡ | | | | | ‡
‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡|‡‡‡‡‡‡‡‡‡‡‡‡|‡‡‡‡‡‡‡‡‡‡‡‡‡‡|‡‡‡‡‡‡‡‡‡‡‡‡|‡‡‡‡‡‡‡‡‡‡|‡‡‡‡‡‡‡‡‡‡‡
‡ ‡ | | | | | ‡
‡ ROTATE ‡ adjust | scroll | — | — | — | - ‡
‡ ENCODER ‡ volume | clips | | | | ‡
‡ KNOB ‡ | | | | | ‡
‡ ‡ | | | | | ‡
‡-----------‡------------+------------+--------------+------------+----------+----------‡
‡ ‡ | | | | | ‡
‡ CLICK ‡ toggle | play | toggle | play | play | toggle ‡
‡ BUTTON ‡ mute | current | play/pause | previous | next | debug ‡
‡ ‡ on/off | clip | mode | clip | clip | mode ‡
‡ ‡ | | | | | ‡
‡-----------‡------------+------------+--------------+------------+----------‡----------‡
‡ ‡ | | | | | ‡
‡ LONG ‡ ∅ | ∅ | toggle | ∅ | ∅ | ∅ ‡
‡ CLICK ‡ | | autoplay | | | ‡
‡ BUTTON ‡ | | mode | | | ‡
‡ ‡ | | | | | ‡
‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡‡|‡‡‡‡‡‡‡‡‡‡‡‡|‡‡‡‡‡‡‡‡‡‡‡‡‡‡|‡‡‡‡‡‡‡‡‡‡‡‡|‡‡‡‡‡‡‡‡‡‡|‡‡‡‡‡‡‡‡‡‡‡
*/
/*
OLED display layout
| 111111111122 // character position
|0123456789012345678901 //
+----------------------+
0 |<album title> |
1 |••••••••••••••••••••••|
2 |* <status> <rem> | // playing clip info
3 |<clip title> | //
4 |••••••••••••••••••••••|
5 |Clip <num> <dur> | // selected clip info
6 |<clip title> | //
7 |<clip note> | //
+----------------------+
* : autoplay on/off
<status>: playing/paused/blank(none)
<rem> : remaining time to play (MM:SS)
<dur> : clip duration (MM:SS)
*/
// libraries used // Purpose // Source
// --------------------------------------------------------------------------------------------------
#include <Adafruit_VS1053.h> // MP3 player & SD card // Adafruit (use Arduino IDE)
#include <Adafruit_SSD1306.h> // OLED display // Adafruit (use Arduino IDE)
#include <Adafruit_NeoPixel.h> // NeoPixel strips // Adafruit (use Arduino IDE)
#include <RTClib.h> // real-time clock // Adafruit (use Arduino IDE)
#include <RotaryEncoder.h> // rotary encoder access // Matthias Hertel (use Arduino IDE)
#include <Fizzlab_Button.h> // high level button use // Fizzlab (fizzlab.cc/software-libraries)
#include <Fizzlab_Timer.h> // async timer // Fizzlab (fizzlab.cc/software-libraries)
// program constants
const String PROGRAM_NAME = "Shell House Jukebox";
const String VERSION_NUM = "0.7";
// define pin numbers for UI elements
const int INDICATOR_LED_RED = D86; // on-board RGB LED (common anode)
const int INDICATOR_LED_GRN = D87; // on-board RGB LED (common anode) = LED_BUILTIN
const int INDICATOR_LED_BLU = D88; // on-board RGB LED (common anode)
const int LED_PIN = LED_BUILTIN;
const int BUZZER_PIN = A6;
const int VOLUME_ENCODER_A_PIN = A0; // VOLUME encoder
const int VOLUME_ENCODER_B_PIN = A1;
const int VOLUME_ENCODER_BTN_PIN = A7;
const int SELECT_ENCODER_A_PIN = A2; // SELECT encoder
const int SELECT_ENCODER_B_PIN = A3;
const int SELECT_ENCODER_BTN_PIN = A4;
const int VOLUME_NEO_STRIP_PIN = D48;
const int AUTOPLAY_NEO_STRIP_PIN = D53;
const int PLAY_PAUSE_NEO_STRIP_PIN = A5;
const int PLAY_PAUSE_BTN_PIN = D50;
const int PREV_BTN_PIN = D51;
const int NEXT_BTN_PIN = D52;
const int SYSTEM_BTN_PIN = D100; // on-board BOOT0 button
// define pin numbers for MP3/SD device (VS1053 breakout board)
const int PLAYER_RST = D7; // VS1053 reset pin (output)
const int PLAYER_CS = D6; // VS1053 chip select pin (output)
const int PLAYER_XDCS = D4; // VS1053 data/command select pin (output)
const int PLAYER_SDCS = D5; // SD card chip select pin
const int PLAYER_DREQ = D3; // VS1053 data request, ideally an Interrupt pin
// see http://arduino.cc/en/Reference/attachInterrupt
// Connect SCLK, MISO/CIPO and MOSI/COPI to hardware SPI pins on ICSP connector
// See http://arduino.cc/en/Reference/SPI "Connections"
// OLED display and RTC use I2C pins CLK & DAT with WIRE1
// some special ASCII characters
const byte HT = 9; // ASCII value (decimal) of the horizontal tab character (TAB) in a text file
const byte LF = 10; // ASCII value (decimal) of the line terminator character (LINE FEED) in a text file
// string constants
const String NULL_STRING = "";
const String DASH = "-";
const String SPACE = " ";
const String METAFILE_NAME = "JUKEBOX.TXT";
const String CLIPFILE_SUBDIR = "CLIPS/";
// named colors for NeoPixels -- values from:
// docs.circuitpython.org/projects/led-animation/en/latest/api.html#adafruit-led-animation-color
typedef unsigned long NeoColor; // packs RGB values into one number
const NeoColor NEO_OFF = Adafruit_NeoPixel::Color(0, 0, 0);
const NeoColor NEO_AMBER = Adafruit_NeoPixel::Color(255, 100, 0);
const NeoColor NEO_AQUA = Adafruit_NeoPixel::Color(50, 255, 255);
const NeoColor NEO_BLUE = Adafruit_NeoPixel::Color(0, 0, 255);
const NeoColor NEO_BLACK = Adafruit_NeoPixel::Color(0, 0, 0);
const NeoColor NEO_CYAN = Adafruit_NeoPixel::Color(0, 255, 255);
const NeoColor NEO_GOLD = Adafruit_NeoPixel::Color(255, 222, 30);
const NeoColor NEO_GREEN = Adafruit_NeoPixel::Color(0, 255, 0);
const NeoColor NEO_JADE = Adafruit_NeoPixel::Color(0, 255, 40);
const NeoColor NEO_MAGENTA = Adafruit_NeoPixel::Color(255, 0, 20);
const NeoColor NEO_OLD_LACE = Adafruit_NeoPixel::Color(253, 245, 230);
const NeoColor NEO_ORANGE = Adafruit_NeoPixel::Color(255, 40, 0);
const NeoColor NEO_PINK = Adafruit_NeoPixel::Color(242, 90, 255);
const NeoColor NEO_PURPLE = Adafruit_NeoPixel::Color(180, 0, 255);
const NeoColor NEO_RED = Adafruit_NeoPixel::Color(255, 0, 0);
const NeoColor NEO_TEAL = Adafruit_NeoPixel::Color(0, 255, 120);
const NeoColor NEO_WHITE = Adafruit_NeoPixel::Color(255, 255, 255);
const NeoColor NEO_YELLOW = Adafruit_NeoPixel::Color(255, 150, 0);
// NeoPixel strip parameters
const int VOLUME_NEO_STRIP_PIXELS = 8;
const int PLAY_PAUSE_NEO_STRIP_PIXELS = 1;
const int AUTOPLAY_NEO_STRIP_PIXELS = 1;
const int NEOPIXEL_BRIGHTNESS = 25;
// OLED display constants
const int OLED_I2C_ADDR = 0x3D; // I2C address
const int OLED_RESET = -1;
const int OLED_WIDTH = 128;
const int OLED_HEIGHT = 64;
// with type size = 1, each glyph is 8 pixels tall x 6 pixels wide
const int OLED_GLYPH_HEIGHT = 8;
const int OLED_GLYPH_WIDTH = 6;
const int OLED_LINES = OLED_HEIGHT / OLED_GLYPH_HEIGHT;
const int OLED_CHARS = OLED_WIDTH / OLED_GLYPH_WIDTH;
int OLED_LINE[OLED_LINES]; // OLED line number pixel addresses, calculated in setup()
int OLED_CHAR[OLED_CHARS]; // OLED character number pixel addresses, calculated in setup()
// splash screen logo
// Greek letter Phi image, 40x40px, made with image2cpp (javl.github.io/image2cpp) and a lot of fiddling
const int PHI_BITMAP_WIDTH = 40;
const int PHI_BITMAP_HEIGHT = 40;
const unsigned char PHI_BITMAP[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0xaa, 0x00, 0x00,
0x10, 0x01, 0x11, 0x00, 0x00, 0xa8, 0x0a, 0x8a, 0x80, 0x00, 0x44, 0x04, 0x04, 0x40, 0x02, 0xa8,
0x2a, 0x02, 0xa0, 0x00, 0x10, 0x10, 0x00, 0x10, 0x0a, 0xa0, 0xaa, 0x02, 0xa8, 0x04, 0x40, 0x44,
0x00, 0x40, 0x0a, 0xa0, 0xaa, 0x02, 0xa8, 0x11, 0x00, 0x10, 0x01, 0x10, 0x0a, 0x80, 0xaa, 0x02,
0xa8, 0x04, 0x40, 0x44, 0x00, 0x44, 0x2a, 0x80, 0xaa, 0x02, 0xa8, 0x11, 0x00, 0x10, 0x01, 0x10,
0x2a, 0x80, 0xaa, 0x02, 0xa8, 0x04, 0x40, 0x44, 0x00, 0x40, 0x2a, 0x80, 0xaa, 0x02, 0xa8, 0x11,
0x00, 0x10, 0x00, 0x10, 0x0a, 0xa0, 0xaa, 0x02, 0xa8, 0x04, 0x40, 0x44, 0x04, 0x40, 0x0a, 0xa0,
0xaa, 0x0a, 0xa0, 0x00, 0x10, 0x10, 0x00, 0x00, 0x02, 0xa8, 0xaa, 0xaa, 0x80, 0x00, 0x44, 0x44,
0x44, 0x00, 0x00, 0x2a, 0xaa, 0xa8, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x00,
0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00,
0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// album parameters
const int MAX_AUDIO_CLIPS = 60;
const int MAX_FILENAME_LEN = 60; // must be at least 13 (8.3 filename format + NULL, but will include full pathnames)
// volume parameters
const int VOLUME_LEVEL[VOLUME_NEO_STRIP_PIXELS + 1] = { 255, 84, 72, 60, 48, 36, 24, 12, 0 }; // position 0 is muted (totally off)
const int DEFAULT_VOLUME = 4; // between 0 (off) and VOLUME_NEO_STRIP_PIXELS (max)
// miscellaneous parameters
// specify I2C connection, use "&Wire" for Uno, Mega; use "&Wire1" for Giga
//#define I2C_CONNECTION &Wire1
const int SERIAL_BAUD_RATE = 19200;
const int SERIAL_TIMEOUT = 2000; // ms to wait for serial monitor to respond
const int INDICATOR_FLASH_TIME = 2500; // ms for temporary displays
// info for each audio clip
struct ClipInfo {
String filename;
unsigned long duration; // total seconds
String title;
String note;
};
// info for the whole jukebox album of clips
struct AlbumInfo {
String title;
int numClips;
ClipInfo clip[MAX_AUDIO_CLIPS];
};
// for parsing metafile text lines
enum MetafileLineKind { ALBUM_TITLE,
CLIP_INFO,
COMMENT };
struct MetafileLine {
MetafileLineKind kind;
String albumTitle;
ClipInfo clipInfo;
};
// instantiate music player object, using hardware SPI pins on ICSP header
Adafruit_VS1053_FilePlayer player = Adafruit_VS1053_FilePlayer(PLAYER_RST, PLAYER_CS, PLAYER_XDCS, PLAYER_DREQ, PLAYER_SDCS);
// instantiate rotary encoder objects
RotaryEncoder volumeEncoder(VOLUME_ENCODER_A_PIN, VOLUME_ENCODER_B_PIN, RotaryEncoder::LatchMode::FOUR3);
RotaryEncoder selectEncoder(SELECT_ENCODER_A_PIN, SELECT_ENCODER_B_PIN, RotaryEncoder::LatchMode::FOUR3);
// instantiate button objects
Fizzlab_Button systemBtn(SYSTEM_BTN_PIN, INPUT);
Fizzlab_Button selectBtn(SELECT_ENCODER_BTN_PIN, INPUT_PULLUP);
Fizzlab_Button volumeBtn(VOLUME_ENCODER_BTN_PIN, INPUT_PULLUP);
Fizzlab_Button playPauseBtn(PLAY_PAUSE_BTN_PIN, INPUT_PULLUP);
Fizzlab_Button prevBtn(PREV_BTN_PIN, INPUT_PULLUP);
Fizzlab_Button nextBtn(NEXT_BTN_PIN, INPUT_PULLUP);
// instantiate NeoPixel strip objects
Adafruit_NeoPixel volumeNeo(VOLUME_NEO_STRIP_PIXELS, VOLUME_NEO_STRIP_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel playPauseNeo(PLAY_PAUSE_NEO_STRIP_PIXELS, PLAY_PAUSE_NEO_STRIP_PIN, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel autoplayNeo(AUTOPLAY_NEO_STRIP_PIXELS, AUTOPLAY_NEO_STRIP_PIN, NEO_GRB + NEO_KHZ800);
// instantiate other objects
Adafruit_SSD1306 oled(OLED_WIDTH, OLED_HEIGHT, &Wire1, OLED_RESET);
RTC_DS3231 rtc;
// program mode variables
bool debugging = true;
bool autoplayOn = true;
bool audioMuted = false;
// program state variables, initially set in initDevices()
int selectEncoderPosition;
int volumeEncoderPosition;
int currentVolumePosition;
int currentClipSelectedIndex;
int currentClipPlayingIndex;
// the data gathered from the metafile that defines the jukebox album on the SD card
AlbumInfo album;
/*
Let the action begin…
*/
void setup() {
// configure pins not set by libraries
pinMode(LED_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(INDICATOR_LED_RED, OUTPUT);
pinMode(INDICATOR_LED_GRN, OUTPUT);
pinMode(INDICATOR_LED_BLU, OUTPUT);
// initialize OLED parameters
for (int i = 0; i < OLED_LINES; i++) {
OLED_LINE[i] = i * OLED_GLYPH_HEIGHT;
}
for (int i = 0; i < OLED_CHARS; i++) {
OLED_CHAR[i] = i * OLED_GLYPH_WIDTH;
}
startSerialMonitor(SERIAL_BAUD_RATE);
Serial.print(F("\n\nInitializing "));
Serial.println(PROGRAM_NAME);
// check and initialize all device components
bool devicesInitOK = initDevices();
// confirm go/no-go on NeoPixels
confirmInitDevices(devicesInitOK, INDICATOR_FLASH_TIME);
// issue startup messages
String programID = "Starting " + PROGRAM_NAME + F(", version ") + VERSION_NUM;
Serial.println(programID);
if (debugging) {
// list all files found on the SD card
File root = SD.open("/");
Serial.println(F("--- Files:"));
printDirectory(root, 0);
Serial.println(F("---"));
root.close();
}
// load clips album information from metadata file
album = loadAlbumInfo();
// print out the album summary
printAlbumInfo(album);
showStaticDisplayInfo();
updateStatusIndicators();
updateVolumeLevel(currentVolumePosition)
Serial.print(PROGRAM_NAME);
Serial.println(" ready @ " + getTimeStamp(rtc.now()));
} // setup
/*
And now the real work…
*/
void loop() {
selectEncoder.tick();
volumeEncoder.tick();
// first check on encoders to update from any movement
int newPosition;
// check selector encoder for displaying clips available to play
newPosition = selectEncoder.getPosition();
// make sure the position isn't less than 1 or greater than the max number of clips in this album
newPosition = max(newPosition, 1);
newPosition = min(newPosition, album.numClips);
// see if it has moved
if (newPosition != selectEncoderPosition) {
selectEncoderPosition = newPosition;
currentClipSelectedIndex = selectEncoderPosition - 1; // array in which it is used is zero-based index
updateSelectedClipDisplayInfo();
if (debugging) {
Serial.print(F("SELECTOR position: "));
Serial.print(selectEncoderPosition);
Serial.print(F(" = index "));
Serial.print(currentClipSelectedIndex);
Serial.print(F(" - '"));
Serial.print(album.clip[currentClipSelectedIndex].title);
Serial.println(F("'"));
}
}
// check volume encoder for adjusting the playback volume
newPosition = volumeEncoder.getPosition();
// make sure the position isn't less than 1 or greater than the max number pixels on the volume NeoPixel strip
newPosition = max(newPosition, 1);
newPosition = min(newPosition, VOLUME_NEO_STRIP_PIXELS);
// see if it has moved
if (newPosition != volumeEncoderPosition) {
volumeEncoderPosition = newPosition;
currentVolumePosition = volumeEncoderPosition;
updateVolumeLevel(currentVolumePosition);
if (debugging) {
Serial.print(F("VOLUME position: "));
Serial.print(currentVolumePosition);
Serial.print(F(" = player level "));
Serial.println(VOLUME_LEVEL[currentVolumePosition]);
}
}
// look for button clicks on the encoders or regular buttons
int action;
// first, the SELECT button
action = selectBtn.checkButtonAction();
if (action == Fizzlab_Button::CLICKED) {
tone(BUZZER_PIN, 440, 100);
if (debugging) {
Serial.print(F("button clicked on pin "));
Serial.println(selectBtn.getPinNumber());
}
currentClipPlayingIndex = currentClipSelectedIndex;
startClip(currentClipPlayingIndex);
updatePlayingClipDisplayInfo();
if (debugging) {
Serial.print(F("play clip # "));
Serial.print(currentClipPlayingIndex + 1);
Serial.print(F(": "));
Serial.print(album.clip[currentClipPlayingIndex].filename);
Serial.print(F(" = '"));
Serial.print(album.clip[currentClipPlayingIndex].title);
Serial.print(F("' ("));
Serial.print(durationText(album.clip[currentClipPlayingIndex].duration));
Serial.println(F(")"));
}
} else {
// nothing happened with the button, so do nothing
} // select btn
// then the VOLUME button
action = volumeBtn.checkButtonAction();
if (action == Fizzlab_Button::CLICKED) {
tone(BUZZER_PIN, 440, 100);
if (debugging) {
Serial.print(F("button clicked on pin "));
Serial.println(volumeBtn.getPinNumber());
}
audioMuted = !audioMuted;
updateStatusIndicators();
updateVolumeLevel(audioMuted ? 0 : currentVolumePosition);
if (debugging) {
Serial.println(audioMuted ? F("audio muted") : F("audio on"));
}
} else {
// nothing happened with the button, so do nothing
} // volume btn
// now the Play|Pause button
action = playPauseBtn.checkButtonAction();
if (action == Fizzlab_Button::CLICKED) {
tone(BUZZER_PIN, 440, 100);
if (debugging) {
Serial.print(F("button clicked on pin "));
Serial.println(playPauseBtn.getPinNumber());
}
// kind of cumbersome looking code here because the library methods:
// player.paused() indicates whether a clip is playing or not (paused)
// player.pausePlaying(bool) plays or pauses the clip
bool clipPlaying = !player.paused();
player.pausePlaying(clipPlaying);
clipPlaying = !player.paused();
updateStatusIndicators();
if (debugging) {
Serial.println(clipPlaying ? "play clip" : "pause clip");
}
} else if (action == Fizzlab_Button::LONG_CLICKED) {
tone(BUZZER_PIN, 440, 100);
if (debugging) {
Serial.print(F("button long licked on pin "));
Serial.println(PLAY_PAUSE_BTN_PIN);
}
autoplayOn = !autoplayOn;
updateStatusIndicators();
if (debugging) {
Serial.println(autoplayOn ? F("autoplay on") : F("autoplay off"));
}
} else {
// nothing happened with the button, so do nothing
} // play|pause btn
// check for clicks on the NEXT / PREV buttons
// first the PREVious
action = prevBtn.checkButtonAction();
if (action == Fizzlab_Button::CLICKED) {
tone(BUZZER_PIN, 440, 100);
if (debugging) {
Serial.print(F("button clicked on pin "));
Serial.println(prevBtn.getPinNumber());
}
int newClipIndex = currentClipPlayingIndex - 1;
// make sure the previous clip isn't less than 1
newClipIndex = max(newClipIndex, 1);
if (newClipIndex != currentClipPlayingIndex) {
currentClipPlayingIndex = newClipIndex;
startClip(currentClipPlayingIndex);
updatePlayingClipDisplayInfo();
if (debugging) {
Serial.print(F("play previous clip # "));
Serial.print(currentClipPlayingIndex + 1);
Serial.print(F(": "));
Serial.print(album.clip[currentClipPlayingIndex].filename);
Serial.print(F(" = '"));
Serial.print(album.clip[currentClipPlayingIndex].title);
Serial.print(F("' ("));
Serial.print(durationText(album.clip[currentClipPlayingIndex].duration));
Serial.println(F(")"));
}
}
} else {
// nothing happened with the button, so do nothing
} // prev btn
// then the NEXT
action = nextBtn.checkButtonAction();
if (action == Fizzlab_Button::CLICKED) {
tone(BUZZER_PIN, 440, 100);
if (debugging) {
Serial.print(F("button clicked on pin "));
Serial.println(nextBtn.getPinNumber());
}
int newClipIndex = currentClipPlayingIndex + 1;
// make sure the previous clip isn't greater than the max number of clips in this album
newClipIndex = min(newClipIndex, album.numClips);
if (newClipIndex != currentClipPlayingIndex) {
currentClipPlayingIndex = newClipIndex;
startClip(currentClipPlayingIndex);
updatePlayingClipDisplayInfo();
if (debugging) {
Serial.print(F("play next clip # "));
Serial.print(currentClipPlayingIndex + 1);
Serial.print(F(": "));
Serial.print(album.clip[currentClipPlayingIndex].filename);
Serial.print(F(" = '"));
Serial.print(album.clip[currentClipPlayingIndex].title);
Serial.print(F("' ("));
Serial.print(durationText(album.clip[currentClipPlayingIndex].duration));
Serial.println(F(")"));
}
}
} else {
// nothing happened with the button, so do nothing
} // next btn
// last, check for clicks on the SYSTEM button
action = systemBtn.checkButtonAction();
if (action == Fizzlab_Button::CLICKED) {
tone(BUZZER_PIN, 440, 100);
if (debugging) {
Serial.print(F("button clicked on pin "));
Serial.println(systemBtn.getPinNumber());
}
debugging = !debugging;
updateStatusIndicators();
if (debugging) {
Serial.println(debugging ? F("debugging on") : F("debugging off"));
}
} else {
// nothing happened with the button, so do nothing
} // system btn
// finally see if clip has finished and check for autoplay to advance to the next clip
// Serial.println("loop-done");
} // loop
bool initDevices() {
bool devicesOK = true;
// check on OLED display
if (oled.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDR)) {
Serial.println(F("OLED display init OK"));
// set display parameters
oled.setTextSize(1);
oled.setTextColor(SSD1306_WHITE);
oled.setTextWrap(false);
oled.clearDisplay();
oled.display();
} else {
Serial.println(F("OLED display not found"));
devicesOK = false;
}
// check on the real-time clock
Serial.print(F("Real-time clock init "));
// on the GIGA, the default I2C pins are Wire1, not the default Wire0
bool rtcOK = rtc.begin(&Wire1);
Serial.println(rtcOK ? "ok" : "NG");
if (rtcOK) {
// check if RTC needs resetting
if (rtc.lostPower()) {
Serial.println(F("Real-time clock needs to be reset"));
devicesOK = false;
} else {
Serial.println(F("Real-time clock power OK"));
}
} else {
devicesOK = false;
}
// check NeoPixel strips
long start = millis();
if (volumeNeo.begin()) {
Serial.println(F("Volume NeoPixel strip init OK"));
volumeNeo.setBrightness(NEOPIXEL_BRIGHTNESS);
volumeNeo.clear();
for (int i = 0; i < volumeNeo.numPixels(); i++) {
volumeNeo.setPixelColor(i, NEO_WHITE);
}
volumeNeo.show();
} else {
Serial.println(F("Volume NeoPixel strip not found"));
devicesOK = false;
}
if (playPauseNeo.begin()) {
Serial.println(F("Play|Pause NeoPixel strip init OK"));
playPauseNeo.setBrightness(NEOPIXEL_BRIGHTNESS);
playPauseNeo.clear();
for (int i = 0; i < playPauseNeo.numPixels(); i++) {
playPauseNeo.setPixelColor(i, NEO_WHITE);
}
playPauseNeo.show();
} else {
Serial.println(F("Play|Pause NeoPixel strip not found"));
devicesOK = false;
}
if (autoplayNeo.begin()) {
Serial.println(F("Autoplay NeoPixel strip init OK"));
autoplayNeo.setBrightness(NEOPIXEL_BRIGHTNESS);
autoplayNeo.clear();
for (int i = 0; i < autoplayNeo.numPixels(); i++) {
autoplayNeo.setPixelColor(i, NEO_WHITE);
}
autoplayNeo.show();
} else {
Serial.println(F("Autoplay NeoPixel strip not found"));
devicesOK = false;
}
while (millis() - start < INDICATOR_FLASH_TIME) {
// stall
}
volumeNeo.clear();
volumeNeo.show();
playPauseNeo.clear();
playPauseNeo.show();
autoplayNeo.clear();
autoplayNeo.show();
// check music player
if (player.begin()) {
Serial.println(F("Music player init OK"));
currentVolumePosition = DEFAULT_VOLUME;
updateVolumeLevel(currentVolumePosition);
player.useInterrupt(VS1053_FILEPLAYER_PIN_INT); // to allow pause/resume of clips and other actions during playing
} else {
Serial.println(F("Music player not found"));
devicesOK = false;
}
// initilaize encoder positions
Serial.println(F("Volume Encoder init OK"));
volumeEncoderPosition = DEFAULT_VOLUME; // does this work?
Serial.println(F("Select Encoder init OK"));
selectEncoderPosition = 0;
// check SD card
if (SD.begin(PLAYER_SDCS)) {
Serial.println(F("SD card init OK"));
} else {
Serial.println(F("SD card not found"));
devicesOK = false;
}
// check if the metadata file for the jukebox exists
Serial.print(F("Metadata file '"));
Serial.print(METAFILE_NAME);
if (SD.exists(METAFILE_NAME)) {
Serial.println(F("' exists"));
} else {
Serial.println(F("' does not exist"));
devicesOK = false;
}
return devicesOK;
} // initDevices
void confirmInitDevices(bool initOK, int flashInterval) {
NeoColor color;
String str;
if (initOK) {
color = NEO_GREEN;
str = F("All devices initialized OK");
} else {
color = NEO_RED;
str = F("All devices NOT initialized OK");
}
Serial.println(str);
for (int i = 0; i < autoplayNeo.numPixels(); i++) {
autoplayNeo.setPixelColor(i, color);
}
for (int i = 0; i < playPauseNeo.numPixels(); i++) {
playPauseNeo.setPixelColor(i, color);
}
for (int i = 0; i < volumeNeo.numPixels(); i++) {
volumeNeo.setPixelColor(i, color);
}
autoplayNeo.show();
playPauseNeo.show();
volumeNeo.show();
if (initOK) {
tone(BUZZER_PIN, 440, flashInterval);
autoplayNeo.clear();
playPauseNeo.clear();
volumeNeo.clear();
} else {
Serial.println(F("Can't continue"));
tone(BUZZER_PIN, 880);
while (true) {
// keep doing nothing
}
}
} // confirmInitDevices
// update volume level
void updateVolumeLevel(int volumePosition) {
volumeNeo.clear();
for (int i = 0; i < volumePosition; i++) {
volumeNeo.setPixelColor(i, NEO_BLUE);
}
volumeNeo.show();
int volumeLevel = VOLUME_LEVEL[volumePosition];
player.setVolume(volumeLevel, volumeLevel);
} // updateVolumeLevel
void updateStatusIndicators() {
// first autoplay mode
autoplayNeo.setPixelColor(0, autoplayOn ? NEO_GREEN : NEO_OFF);
autoplayNeo.show();
// next play|pause status
NeoColor color;
if (player.stopped()) {
color = NEO_OFF;
} else {
color = player.paused() ? NEO_ORANGE : NEO_GREEN;
}
playPauseNeo.setPixelColor(0, color);
playPauseNeo.show();
// and debugging mode
// indicator LED (on-board) is part of an RGB LED with a common anode, so HIGH/LOW are reversed
digitalWrite(INDICATOR_LED_BLU, debugging ? LOW : HIGH);
} // updateStatusIndicators
void printDisplayLine(int lineNum, String str) {
if (lineNum > OLED_LINES - 1) {
Serial.print("??????? printDisplayLine = ");
Serial.println(lineNum);
return;
}
oled.setCursor(OLED_CHAR[0], OLED_LINE[lineNum]);
oled.print(str);
for (int i = str.length(); i < OLED_CHARS; i++) {
oled.print(SPACE);
}
} // printDisplayLine
void showStaticDisplayInfo() {
// line 0 -- album title
printDisplayLine(0, album.title);
// make a dashed line of the right length for the display line
String dashes = NULL_STRING;
for (int i = 0; i < OLED_CHARS; i++) {
dashes += DASH;
}
// lines 1 & 4 -- dashed separator
printDisplayLine(1, dashes);
printDisplayLine(4, dashes);
oled.display();
} // showStaticDisplayInfo
void updatePlayingClipDisplayInfo() {
String str = NULL_STRING;
// line 2 -- autoplay status, current playing status, time remaining
oled.setCursor(OLED_CHAR[0], OLED_LINE[2]);
oled.print(autoplayOn ? "*" : " ");
if (player.playingMusic) {
oled.setCursor(OLED_CHAR[2], OLED_LINE[2]);
str = player.paused() ? "Paused " : "Playing";
oled.print(str);
oled.setCursor(OLED_CHAR[15], OLED_LINE[2]);
// print remaining time on currently playing clip
}
// line 3 -- currently playing clip title
oled.setCursor(OLED_CHAR[0], OLED_LINE[3]);
if (player.playingMusic) {
oled.print(album.clip[currentClipPlayingIndex].title);
}
oled.display();
} // updatePlayingClipDisplayInfo
void updateSelectedClipDisplayInfo() {
// the last 3 lines show which clip is currently selected (and ready to play)
String str = NULL_STRING;
// construct line 5 -- currently selected clip number & duration
str = "Clip ";
int clipNum = currentClipSelectedIndex + 1;
if (clipNum < 10) {
str += "0";
}
str += String(clipNum);
str += SPACE;
str += durationText(album.clip[currentClipSelectedIndex].duration);
printDisplayLine(5, str);
// line 6 -- currently selected clip title
str = album.clip[currentClipSelectedIndex].title;
printDisplayLine(6, str);
// line 7 -- currently selected clip note
str = album.clip[currentClipSelectedIndex].note;
printDisplayLine(7, str);
oled.display();
} // updateSelectedClipDisplayInfo
AlbumInfo loadAlbumInfo() {
AlbumInfo a;
// check if the meta data file for the Jukebox is available
File metadata = SD.open(METAFILE_NAME);
if (!metadata) {
Serial.print(F("Can not open metadata file. Can not continue."));
while (true) {
delay(10);
}
}
// get ready to process it
Serial.print(F("=== Processing metadata file = '"));
Serial.print(metadata.name());
Serial.println(F("' :"));
int linesProcessed = 0;
int clipsProcessed = 0;
int clipsUsed = 0;
int clipsSkipped = 0;
int clipIndex = 0;
String line = NULL_STRING;
// read the file byte by byte, processing a line at a time
while (metadata.available()) {
byte ch = metadata.read();
if (ch == LF) { // end of line
// now we can parse that line
linesProcessed++;
MetafileLine metafileLine = parseLine(line);
// and then load info on that line into the album data structure
switch (metafileLine.kind) {
case ALBUM_TITLE:
// if more than one, just use the latest
a.title = metafileLine.albumTitle;
break;
case CLIP_INFO:
clipsProcessed++;
if (checkClipOK(metafileLine.clipInfo, clipsProcessed)) {
clipsUsed++;
a.clip[clipIndex] = metafileLine.clipInfo;
clipIndex++;
} else {
clipsSkipped++;
}
break;
default:
// skip comments and other unrecognized line kinds
break;
}
// finished that line, so start the next one
line = NULL_STRING;
} else { // character just read is not the end of the line
// just keep accumulating characters in the line
line += char(ch);
}
}
metadata.close();
Serial.print(F("=== Lines processed: "));
Serial.println(linesProcessed);
Serial.print(F("=== Clips processed: "));
Serial.println(clipsProcessed);
Serial.print(F("=== Clips used: "));
Serial.println(clipsUsed);
Serial.print(F("=== Clips skipped: "));
Serial.println(clipsSkipped);
Serial.println();
a.numClips = clipIndex; // because it points to the next index to be used
// which is the number of clips used (zero-based index)
return a;
} //loadAlbumInfo
MetafileLine parseLine(String line) {
MetafileLine lineInfo;
String payload = line.substring(1);
if (line.startsWith("*")) {
Serial.print(F("*: "));
Serial.println(payload);
lineInfo.kind = ALBUM_TITLE;
lineInfo.albumTitle = payload;
} else if (line.startsWith("+")) {
Serial.print(F("+: "));
Serial.println(payload);
lineInfo.kind = CLIP_INFO;
lineInfo.clipInfo = parseClipLine(payload);
} else {
// ignore "#" and anything else
Serial.print(F("#: "));
Serial.println(payload);
lineInfo.kind = COMMENT;
}
return lineInfo;
} // parseLine
ClipInfo parseClipLine(String line) {
ClipInfo c;
// each line has four token sub-strings, separated by tabs
// so we separate them out into an array of tokens
int start = 0;
int delimiter;
String token[4];
for (int i = 0; i < 4; i++) {
delimiter = line.indexOf(HT, start);
token[i] = line.substring(start, delimiter);
start = delimiter + 1;
}
// then process them and load them into a structure to return to the caller
// assuming the duration is a text string in the form of "MM:SS"
// so we convert that to the equivalent number of seconds as an integer
// ********** change to accept HH:MM:SS for duration
c.filename = CLIPFILE_SUBDIR + token[0];
int minutes = token[1].substring(0, 2).toInt();
int seconds = token[1].substring(3).toInt();
c.duration = minutes * 60 + seconds;
c.title = token[2];
c.note = token[3];
return c;
} // parseClipLine
// verify clip info, return true if OK, false if it is to be skipped
bool checkClipOK(ClipInfo clip, int clipCount) {
// check for too many clips
if (clipCount > MAX_AUDIO_CLIPS) {
Serial.println("??? Clip # " + String(clipCount) + F(" exceeds max of ") + String(MAX_AUDIO_CLIPS) + F(". Ignoring this one."));
return false;
}
// make sure it exists on the SD card
if (!SD.exists(clip.filename)) {
Serial.println("??? Clip # " + String(clipCount) + F(" file = '") + clip.filename + F("' not found. Ignoring this one."));
return false;
}
// make sure it is an MP3 file
// annoyingly, the player library does not accept Strings for filenames, only arrays of chars
char filename[MAX_FILENAME_LEN] = "";
clip.filename.toCharArray(filename, MAX_FILENAME_LEN);
if (!player.isMP3File(filename)) {
Serial.println("??? Clip # " + String(clipCount) + F(" file = '") + clip.filename + F("' is not an MP3 file. Ignoring this one."));
return false;
}
// got here, so all OK
return true;
} // checkClipOK
void startClip(int clipIndex) {
// annoyingly, the player library does not accept Strings for filenames, only arrays of chars
char filename[MAX_FILENAME_LEN] = "";
album.clip[clipIndex].filename.toCharArray(filename, MAX_FILENAME_LEN);
player.stopPlaying();
player.startPlayingFile(filename);
} // startClip
// returns text version of the date & time, formatted as "YYYY-MM-DD HH:MM:SS"
String getTimeStamp(DateTime now) {
return (getCurrentDate(now) + " " + getCurrentTime(now));
}
// returns text version of the date, formatted as "YYYY-MM-DD"
String getCurrentDate(DateTime now) {
char str[15]; // one more than the actual characters, to allow for the NULL terminator
sprintf(str, "%4d-%02d-%02d", now.year(), now.month(), now.day());
return String(str);
}
// returns text version of the time, formatted as "HH:MM:SS"
String getCurrentTime(DateTime now) {
char str[15]; // one more than the actual characters, to allow for the NULL terminator
sprintf(str, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
return String(str);
}
// returns text version of the duration (seconds), formatted as "MM:SS"
String durationText(long duration) { // in seconds
// ********* change this to allow HH:MM:SS ???
char str[15]; // one more than the actual characters, to allow for the NULL terminator
int minutes = duration / 60;
int seconds = duration % 60;
sprintf(str, "%02d:%02d", minutes, seconds);
return String(str);
}
void printAlbumInfo(AlbumInfo a) {
Serial.print(F("• ### Album meta data in "));
Serial.println(METAFILE_NAME);
Serial.print(F("• Title: "));
Serial.println(a.title);
Serial.print(F("• Clips: "));
Serial.println(a.numClips);
for (int i = 0; i < a.numClips; i++) {
Serial.print(F("• Clip # "));
Serial.println(i + 1); // adjust for zero-based array index
Serial.print(F("•\tfilename = "));
Serial.println(a.clip[i].filename);
Serial.print(F("•\tduration = "));
Serial.print(a.clip[i].duration);
Serial.print(F(" ("));
Serial.print(durationText(a.clip[i].duration));
Serial.println(F(")"));
Serial.print(F("•\ttitle = "));
Serial.println(a.clip[i].title);
Serial.print(F("•\tnote = "));
Serial.println(a.clip[i].note);
}
Serial.println(F("• ###"));
}
// from SD library example 'listfiles'
// prints all files in the directory,
// recursively down through all sub-directories
// [a neat trick, but I don't like the while(true) / break construction]
void printDirectory(File dir, int numTabs) {
while (true) {
File entry = dir.openNextFile();
if (!entry) {
// no more files
break;
}
for (uint8_t i = 0; i < numTabs; i++) {
Serial.print('\t');
}
Serial.print(entry.name());
if (entry.isDirectory()) {
Serial.println("/");
printDirectory(entry, numTabs + 1);
} else {
// files have sizes, directories do not
Serial.print("\t\t");
Serial.println(entry.size(), DEC);
}
entry.close();
}
}
void startSerialMonitor(int baudRate) {
digitalWrite(INDICATOR_LED_RED, LOW); // turn on [LOW because this LED is common anode]
// initialize serial port for console messages
Serial.begin(baudRate);
// GIGA might need extra time to initialize the serial monitor, so wait for it.
// But if the GIGA is not connected via USB to a serial monitor (power only),
// we just wait for it to time out and then continue on without a serial monitor.
long serialTimerStart = millis();
while (!Serial && (millis() - serialTimerStart < SERIAL_TIMEOUT)) {
// just stall
}
digitalWrite(INDICATOR_LED_RED, HIGH); // turn off [HIGH because this LED is common anode]
}