diff --git a/sound_picture_app/Audio_Player.cpp b/sound_picture_app/Audio_Player.cpp index 6637e0a..bf5ed5e 100644 --- a/sound_picture_app/Audio_Player.cpp +++ b/sound_picture_app/Audio_Player.cpp @@ -1,186 +1,94 @@ #include "Audio_Player.h" -#include -#include +#include "Audio.h" -I2SClass i2s; +Audio audio; TaskHandle_t audioTaskHandle = NULL; -File audioFile; -volatile bool isAudioPlaying = false; -char currentPlayPath[128]; +volatile bool isAudioTaskRunning = false; +uint8_t currentVolume = 12; // 0 to 21 range -uint16_t Wav_Count = 0; -char Wav_Names[100][100]; +uint16_t Audio_Count = 0; +char Audio_Names[100][100]; -void Search_Wavs(const char* directory) { - Wav_Count = Folder_retrieval(directory, ".wav", Wav_Names, 100); -} - - -// Parse WAV header and skip meta chunks (like LIST) to find the "fmt " and "data" segments -bool parseWavHeader(File& file, uint32_t& sampleRate, uint16_t& channels, uint16_t& bitsPerSample, uint32_t& dataStartOffset, uint32_t& dataSize) { - char id[4]; - - // Read RIFF header - file.seek(0); - if (file.readBytes(id, 4) != 4 || memcmp(id, "RIFF", 4) != 0) { - printf("WAV Header Error: RIFF signature not found!\r\n"); - return false; +// FreeRTOS background task on Core 0 to run ESP32-audioI2S decode loop +void audioLoopTask(void* parameter) { + isAudioTaskRunning = true; + while (isAudioTaskRunning) { + audio.loop(); + vTaskDelay(pdMS_TO_TICKS(1)); // Yield to let other RTOS tasks run } - - file.seek(8); - if (file.readBytes(id, 4) != 4 || memcmp(id, "WAVE", 4) != 0) { - printf("WAV Header Error: WAVE signature not found!\r\n"); - return false; - } - - uint32_t offset = 12; - bool foundFmt = false; - bool foundData = false; - - while (offset < file.size() && (!foundFmt || !foundData)) { - file.seek(offset); - if (file.readBytes(id, 4) != 4) break; - - uint32_t chunkSize; - if (file.read((uint8_t*)&chunkSize, 4) != 4) break; - - offset += 8; // move past chunk ID and Size field - - if (memcmp(id, "fmt ", 4) == 0) { - uint16_t audioFormat; - file.read((uint8_t*)&audioFormat, 2); - if (audioFormat != 1) { // 1 = PCM (uncompressed) - printf("WAV Header Error: Only uncompressed PCM WAV is supported!\r\n"); - return false; - } - file.read((uint8_t*)&channels, 2); - file.read((uint8_t*)&sampleRate, 4); - file.seek(offset + 8); // skip byte rate and block align - file.read((uint8_t*)&bitsPerSample, 2); - foundFmt = true; - } - else if (memcmp(id, "data", 4) == 0) { - dataStartOffset = offset; - dataSize = chunkSize; - foundData = true; - } - - offset += chunkSize; - } - - return foundFmt && foundData; -} - -void audioPlayTask(void* parameter) { - uint32_t sampleRate = 44100; - uint16_t channels = 2; - uint16_t bitsPerSample = 16; - uint32_t dataStartOffset = 44; - uint32_t dataSize = 0; - - audioFile = SD_MMC.open(currentPlayPath, "r"); - if (!audioFile) { - printf("Failed to open audio file: %s\r\n", currentPlayPath); - isAudioPlaying = false; - audioTaskHandle = NULL; - vTaskDelete(NULL); - return; - } - - if (!parseWavHeader(audioFile, sampleRate, channels, bitsPerSample, dataStartOffset, dataSize)) { - printf("Failed to parse WAV header\r\n"); - audioFile.close(); - isAudioPlaying = false; - audioTaskHandle = NULL; - vTaskDelete(NULL); - return; - } - - printf("WAV File: Rate=%d Hz, Channels=%d, Bits=%d, Size=%d bytes\r\n", - sampleRate, channels, bitsPerSample, dataSize); - - // Configure I2S based on WAV parameters - i2s_data_bit_width_t bitWidth = I2S_DATA_BIT_WIDTH_16BIT; - if (bitsPerSample == 8) bitWidth = I2S_DATA_BIT_WIDTH_8BIT; - else if (bitsPerSample == 24) bitWidth = I2S_DATA_BIT_WIDTH_24BIT; - else if (bitsPerSample == 32) bitWidth = I2S_DATA_BIT_WIDTH_32BIT; - - i2s_slot_mode_t slotMode = (channels == 1) ? I2S_SLOT_MODE_MONO : I2S_SLOT_MODE_STEREO; - - // Set standard pins - i2s.setPins(I2S_BCLK, I2S_LRCK, I2S_DIN, -1, -1); - - // Stop any active driver configuration - i2s.end(); - - if (!i2s.begin(I2S_MODE_STD, sampleRate, bitWidth, slotMode)) { - printf("Failed to initialize I2S!\r\n"); - audioFile.close(); - isAudioPlaying = false; - audioTaskHandle = NULL; - vTaskDelete(NULL); - return; - } - - // Seek past the parsed headers to raw data - audioFile.seek(dataStartOffset); - - const size_t bufferSize = 1024; - uint8_t buffer[bufferSize]; - uint32_t bytesRead = 0; - uint32_t totalBytesWritten = 0; - - while (isAudioPlaying && totalBytesWritten < dataSize) { - size_t toRead = std::min(bufferSize, (size_t)(dataSize - totalBytesWritten)); - bytesRead = audioFile.read(buffer, toRead); - if (bytesRead == 0) { - break; - } - - size_t written = i2s.write(buffer, bytesRead); - if (written == 0) { - vTaskDelay(pdMS_TO_TICKS(1)); - continue; - } - totalBytesWritten += written; - } - - printf("Playback done. Written %d bytes.\r\n", totalBytesWritten); - audioFile.close(); - i2s.end(); - isAudioPlaying = false; audioTaskHandle = NULL; vTaskDelete(NULL); } -void playWav(const char* filePath) { - if (isAudioPlaying) { - stopWav(); - } - strncpy(currentPlayPath, filePath, sizeof(currentPlayPath) - 1); - currentPlayPath[sizeof(currentPlayPath) - 1] = '\0'; - isAudioPlaying = true; +void initAudioPlayer() { + audio.setPinout(I2S_BCLK, I2S_LRCK, I2S_DIN); + audio.setVolume(currentVolume); - xTaskCreatePinnedToCore( - audioPlayTask, - "audioPlayTask", - 4096, - NULL, - 5, - &audioTaskHandle, - 0 - ); -} - -void stopWav() { - if (isAudioPlaying) { - isAudioPlaying = false; - while (audioTaskHandle != NULL) { - vTaskDelay(pdMS_TO_TICKS(5)); - } + // Start the background loop task on Core 0 + if (audioTaskHandle == NULL) { + xTaskCreatePinnedToCore( + audioLoopTask, + "audioLoopTask", + 8192, // Stack size (ESP32-audioI2S requires a larger stack size for decoding) + NULL, + 5, // Priority + &audioTaskHandle, + 0 // Core 0 + ); } } -bool isWavPlaying() { - return isAudioPlaying; +void playAudio(const char* filePath) { + printf("AudioPlayer: Connecting to FS for file: %s\r\n", filePath); + audio.connecttoFS(SD_MMC, filePath); +} + +void stopAudio() { + audio.stopSong(); +} + +void pauseResumeAudio() { + audio.pauseResume(); +} + +bool isAudioPlaying() { + return audio.isRunning(); +} + +void setPlayerVolume(uint8_t volume) { + if (volume > 21) volume = 21; + currentVolume = volume; + audio.setVolume(currentVolume); + printf("AudioPlayer: Volume set to %d / 21\r\n", currentVolume); +} + +uint8_t getPlayerVolume() { + return currentVolume; +} + +void Search_Audio(const char* directory) { + File Path = SD_MMC.open(directory); + if (!Path) { + printf("Path: <%s> does not exist\r\n", directory); + Audio_Count = 0; + return; + } + + uint16_t fileCount = 0; + File file = Path.openNextFile(); + while (file && fileCount < 100) { + if (!file.isDirectory()) { + const char* name = file.name(); + // Match WAV or MP3 files + if (strstr(name, ".wav") || strstr(name, ".mp3") || strstr(name, ".WAV") || strstr(name, ".MP3")) { + strncpy(Audio_Names[fileCount], name, sizeof(Audio_Names[fileCount]) - 1); + Audio_Names[fileCount][sizeof(Audio_Names[fileCount]) - 1] = '\0'; + fileCount++; + } + } + file = Path.openNextFile(); + } + Path.close(); + Audio_Count = fileCount; + printf("Found %d audio files (.mp3/.wav)\r\n", Audio_Count); } diff --git a/sound_picture_app/Audio_Player.h b/sound_picture_app/Audio_Player.h index 350a45d..7fbc168 100644 --- a/sound_picture_app/Audio_Player.h +++ b/sound_picture_app/Audio_Player.h @@ -8,11 +8,14 @@ #define I2S_LRCK 2 #define I2S_DIN 4 -void playWav(const char* filePath); -void stopWav(); -bool isWavPlaying(); - -void Search_Wavs(const char* directory); -extern uint16_t Wav_Count; -extern char Wav_Names[100][100]; +void initAudioPlayer(); +void playAudio(const char* filePath); +void stopAudio(); +void pauseResumeAudio(); +bool isAudioPlaying(); +void setPlayerVolume(uint8_t volume); +uint8_t getPlayerVolume(); +void Search_Audio(const char* directory); +extern uint16_t Audio_Count; +extern char Audio_Names[100][100]; diff --git a/sound_picture_app/sound_picture_app.ino b/sound_picture_app/sound_picture_app.ino index a4c9b59..f11ab6d 100644 --- a/sound_picture_app/sound_picture_app.ino +++ b/sound_picture_app/sound_picture_app.ino @@ -32,6 +32,13 @@ uint32_t lastLedPulseTime = 0; uint8_t pulseBrightness = 0; bool pulseDirection = true; +// Click gesture timers +uint32_t lastButtonReleaseTime = 0; +uint32_t buttonPressStartTime = 0; +uint8_t clickCount = 0; +bool isButtonPressed = false; +bool longPressTriggered = false; + void setup() { Serial.begin(115200); delay(1000); @@ -55,12 +62,15 @@ void setup() { // Retrieve media files from SD Card Search_Images("/"); - Search_Wavs("/"); + Search_Audio("/"); + + // Initialize ESP32-audioI2S player + initAudioPlayer(); printf("Initialization complete!\r\n"); - printf("Found %d PNG images and %d WAV soundbites.\r\n", Image_Count, Wav_Count); + printf("Found %d PNG images and %d Audio files (.mp3/.wav).\r\n", Image_Count, Audio_Count); - if (Image_Count == 0 || Wav_Count == 0) { + if (Image_Count == 0 || Audio_Count == 0) { hasError = true; currentState = STATE_ERROR; printf("ERROR: No media found on microSD card!\r\n"); @@ -72,13 +82,13 @@ void setup() { } void playCurrentPair() { - if (Image_Count == 0 && Wav_Count == 0) { - printf("Cannot play: missing PNG images and WAV soundbites!\r\n"); + if (Image_Count == 0 && Audio_Count == 0) { + printf("Cannot play: missing PNG images and Audio files!\r\n"); return; } // Stop current playing audio if any - stopWav(); + stopAudio(); printf("Playing item index %d:\r\n", currentIndex); @@ -88,12 +98,76 @@ void playCurrentPair() { Display_Image_By_Index("/", pngIndex); } - // Play the soundbite - if (Wav_Count > 0) { - uint16_t wavIndex = currentIndex % Wav_Count; - String soundPath = String("/") + Wav_Names[wavIndex]; - printf("Playing sound: %s\r\n", soundPath.c_str()); - playWav(soundPath.c_str()); + // Play the audio bite + if (Audio_Count > 0) { + uint16_t audioIndex = currentIndex % Audio_Count; + String soundPath = String("/") + Audio_Names[audioIndex]; + printf("Playing audio: %s\r\n", soundPath.c_str()); + playAudio(soundPath.c_str()); + } +} + +void handleSinglePress() { + printf("Gesture: Single Click - Next Slide\r\n"); + if (!hasError) { + if (currentState == STATE_AUTOPLAY) { + currentState = STATE_IDLE; + printf("Autoplay Disabled.\r\n"); + } + currentIndex = (currentIndex + 1) % std::max(Image_Count, Audio_Count); + playCurrentPair(); + } +} + +void handleDoublePress() { + printf("Gesture: Double Click - Pause / Resume Audio\r\n"); + if (!hasError) { + pauseResumeAudio(); + } +} + +void handleTriplePress() { + printf("Gesture: Triple Click - Previous Slide\r\n"); + if (!hasError) { + if (currentState == STATE_AUTOPLAY) { + currentState = STATE_IDLE; + printf("Autoplay Disabled.\r\n"); + } + uint16_t totalMedia = std::max(Image_Count, Audio_Count); + currentIndex = (currentIndex + totalMedia - 1) % totalMedia; + playCurrentPair(); + } +} + +void handleQuadruplePress() { + if (!hasError) { + uint8_t nextVol = getPlayerVolume() + 4; + if (nextVol > 20) nextVol = 4; // Loop back + setPlayerVolume(nextVol); + + // Quick flash NeoPixel white to indicate volume change + for(int i = 0; i < 3; i++) { + neopixelWrite(PIN_NEOPIXEL, 64, 64, 64); + delay(50); + neopixelWrite(PIN_NEOPIXEL, 0, 0, 0); + delay(50); + } + } +} + +void handleLongPress() { + printf("Gesture: Long Press - Toggle Autoplay Slideshow\r\n"); + if (!hasError) { + if (currentState == STATE_AUTOPLAY) { + currentState = STATE_IDLE; + printf("Autoplay Disabled. Manual Mode active.\r\n"); + stopAudio(); + } else { + currentState = STATE_AUTOPLAY; + printf("Autoplay Enabled! Cycling slides every %d ms.\r\n", slideShowInterval); + lastSlideTime = millis(); + playCurrentPair(); + } } } @@ -121,7 +195,7 @@ void updateLEDStatus() { neopixelWrite(PIN_NEOPIXEL, 0, 0, 0); } } - else if (isWavPlaying()) { + else if (isAudioPlaying()) { // Pulsing Green neopixelWrite(PIN_NEOPIXEL, 0, pulseBrightness, 0); } @@ -137,62 +211,54 @@ void updateLEDStatus() { void loop() { bool currentButtonReading = digitalRead(BOOT_KEY_PIN); - static uint32_t buttonPressStartTime = 0; - static bool buttonWasHeld = false; + uint32_t now = millis(); - // Detect changes in button state - if (currentButtonReading != lastButtonState) { - lastDebounceTime = millis(); - if (currentButtonReading == LOW) { - buttonPressStartTime = millis(); - buttonWasHeld = false; + // Detect state transitions for debouncing and gesture capturing + if (currentButtonReading == LOW && lastButtonState == HIGH) { + // Button Pressed down + lastDebounceTime = now; + buttonPressStartTime = now; + isButtonPressed = true; + longPressTriggered = false; + } + else if (currentButtonReading == LOW && lastButtonState == LOW && isButtonPressed) { + // Button is held down + if (!longPressTriggered && (now - buttonPressStartTime > 1500)) { + longPressTriggered = true; + handleLongPress(); + } + } + else if (currentButtonReading == HIGH && lastButtonState == LOW) { + // Button Released + isButtonPressed = false; + uint32_t pressDuration = now - buttonPressStartTime; + + if (!longPressTriggered && pressDuration > 50) { // Debounced release + clickCount++; + lastButtonReleaseTime = now; } } - if ((millis() - lastDebounceTime) > debounceDelay) { - if (currentButtonReading == LOW) { - // Button is being held down. Check if it exceeds long-press threshold (1.5 seconds) - if (!buttonWasHeld && (millis() - buttonPressStartTime > 1500)) { - buttonWasHeld = true; // Mark as held to prevent repeated triggers - if (!hasError) { - if (currentState == STATE_AUTOPLAY) { - currentState = STATE_IDLE; - printf("Autoplay Disabled. Manual Mode active.\r\n"); - stopWav(); - } else { - currentState = STATE_AUTOPLAY; - printf("Autoplay Enabled! Cycling slides every %d ms.\r\n", slideShowInterval); - lastSlideTime = millis(); - playCurrentPair(); - } - } - } - } else { - // Button was released - if (lastButtonState == LOW && !buttonWasHeld) { - // Single press trigger - printf("Single press detected!\r\n"); - if (!hasError) { - if (currentState == STATE_AUTOPLAY) { - currentState = STATE_IDLE; - printf("Autoplay Disabled via manual navigation.\r\n"); - } - playCurrentPair(); - currentIndex = (currentIndex + 1) % std::max(Image_Count, Wav_Count); - } else { - printf("System in error state (missing files). Can't cycle.\r\n"); - } - } - buttonWasHeld = false; + // Process click gestures after 300ms timeout has elapsed since last release + if (clickCount > 0 && !isButtonPressed && (now - lastButtonReleaseTime > 300)) { + if (clickCount == 1) { + handleSinglePress(); + } else if (clickCount == 2) { + handleDoublePress(); + } else if (clickCount == 3) { + handleTriplePress(); + } else if (clickCount >= 4) { + handleQuadruplePress(); } + clickCount = 0; // Reset counter } // Handle slideshow auto-advance in Autoplay mode if (currentState == STATE_AUTOPLAY && !hasError) { - // Advance if the audio finished playing and 5 seconds has elapsed - if (!isWavPlaying() && (millis() - lastSlideTime > slideShowInterval)) { - lastSlideTime = millis(); - currentIndex = (currentIndex + 1) % std::max(Image_Count, Wav_Count); + // Only advance when the audio stops playing and slideShowInterval (5s) has passed + if (!isAudioPlaying() && (now - lastSlideTime > slideShowInterval)) { + lastSlideTime = now; + currentIndex = (currentIndex + 1) % std::max(Image_Count, Audio_Count); playCurrentPair(); } }