diff --git a/partitions-4m-factory.csv b/partitions-4m-factory.csv new file mode 100644 index 0000000..0c867ea --- /dev/null +++ b/partitions-4m-factory.csv @@ -0,0 +1,4 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 4M \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index e792ec7..86936a8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,8 +2,10 @@ platform = espressif32 board = esp32dev framework = arduino +board_build.partitions = partitions-4m-factory.csv lib_deps = https://github.com/pschatzmann/arduino-snapclient https://github.com/pschatzmann/arduino-audio-tools https://github.com/pschatzmann/arduino-audio-driver https://github.com/pschatzmann/arduino-libopus - https://github.com/bblanchon/ArduinoJson \ No newline at end of file + https://github.com/bblanchon/ArduinoJson + https://github.com/pschatzmann/arduino-libhelix \ No newline at end of file diff --git a/src/Entry.h b/src/Entry.h deleted file mode 100644 index 2c7e557..0000000 --- a/src/Entry.h +++ /dev/null @@ -1,37 +0,0 @@ -#ifndef ENTRY_H -#define ENTRY_H - -#include - -enum Type { - T_UNKNOWN_, - T_SNAP, - T_FILE, - T_HTTP, -}; - -static Type typeFromString(const String &name) { - if (name.equals("SNAP")) { - return T_SNAP; - } - if (name.equals("FILE")) { - return T_FILE; - } - if (name.equals("HTTP")) { - return T_HTTP; - } - Serial.printf("[ERROR] _isPlaying: type not implemented: %s\n", name.c_str()); - return T_UNKNOWN_; -} - -struct Entry { - - Type type; - - String url; - - String title; - -}; - -#endif diff --git a/src/Player.cpp b/src/Player.cpp deleted file mode 100644 index 367a3dc..0000000 --- a/src/Player.cpp +++ /dev/null @@ -1,222 +0,0 @@ -#include "Player.h" - -#include - -Player::Player() : board(AudioKitEs8388V1), opus(), snap(wifi, board, opus) { - // -} - -// ------------------------------------------------------------------------------------------------ - -void Player::loop() { - if (playlistRequest) { - playlistRequest = false; - _loadPlaylist(); - } - if (_isPlaying()) { - switch (should) { - case PLAY: - if (current != wanted) { - _stop(); - } - break; - case PAUSE: - _pause(); - break; - case STOP: - _stop(); - break; - } - } - if (should == PLAY) { - _start(); - } -} - -// ------------------------------------------------------------------------------------------------ - -void Player::loadPlaylist(const String &path) { - playlistPath = path; - playlistRequest = true; -} - -void Player::play() { - if (entries != nullptr && playlistSize > 0 && wanted == nullptr) { - wanted = entries; - } - should = PLAY; -} - -void Player::pause() { - should = PAUSE; -} - -void Player::stop() { - should = STOP; -} - -void Player::next() { - skip(+1); -} - -void Player::previous() { - skip(-1); -} - -void Player::skip(const int count) { - if (entries == nullptr || playlistSize <= 0) { - return; - } - playIndex(((wanted - entries + count) % playlistSize + playlistSize) % playlistSize); -} - -void Player::playIndex(const size_t index) { - if (entries == nullptr || playlistSize <= 0) { - return; - } - wanted = index % playlistSize + entries; - should = PLAY; -} - -// ------------------------------------------------------------------------------------------------ - -void Player::_loadPlaylist() { - playlistRequest = false; - - _stop(); - - if (entries != nullptr) { - free(entries); - entries = nullptr; - } - - if (!LittleFS.exists(playlistPath)) { - Serial.println("Playlist file not found."); - return; - } - - File file = LittleFS.open(playlistPath, "r"); - if (!file) { - Serial.println("Failed to open playlist file."); - return; - } - - JsonDocument playlist; - const auto error = deserializeJson(playlist, file); - file.close(); - - if (error) { - Serial.println("Failed to parse playlist JSON."); - return; - } - - const auto jsonEntries = playlist["entries"].as(); - playlistSize = jsonEntries.size(); - if (playlistSize <= 0) { - Serial.printf("Loaded playlist is empty."); - return; - } - - entries = static_cast(malloc(sizeof(Entry) * playlistSize)); - wanted = entries; - - Entry *entry = entries; - for (auto jsonVar: jsonEntries) { - auto jsonEntry = jsonVar.as(); - if (jsonEntry == NULL) { - Serial.printf("Entry #%03d is not a JsonObject\n", entry - entries); - continue; - } - if (!jsonEntry["type"].is()) { - Serial.printf("Entry #%03d has no String 'type'\n", entry - entries); - continue; - } - if (!jsonEntry["url"].is()) { - Serial.printf("Entry #%03d has no String 'url'\n", entry - entries); - continue; - } - if (!jsonEntry["title"].is()) { - Serial.printf("Entry #%03d has no String 'title'\n", entry - entries); - continue; - } - entry->type = typeFromString(jsonEntry["type"].as()); - entry->url = jsonEntry["url"].as(); - entry->title = jsonEntry["title"].as(); - entry++; - } -} - -bool Player::_isPlaying() { - if (entries == nullptr || current == nullptr) { - return false; - } - switch (current->type) { - case T_SNAP: - return snap.doLoop(); - default: - Serial.printf("[ERROR] _isPlaying: type not implemented: %d\n", current->type); - break; - } - return false; -} - -void Player::_start() { - current = wanted; - if (entries == nullptr || current == nullptr) { - Serial.println("[ERROR] _start: current == null => STOP"); - should = STOP; - return; - } - - bool success = false; - switch (current->type) { - case T_SNAP: - success = _snapStart(current->url); - break; - default: - Serial.printf("[ERROR] _start: type not implemented: %d\n", current->type); - success = false; - break; - } - if (success) { - Serial.printf("Started: #%03d: %s\n", current - entries, current->url.c_str()); - } else { - Serial.printf("Failed to start: #%03d: %s\n", current - entries, current->url.c_str()); - next(); - } -} - -void Player::_pause() { - switch (current->type) { - case T_SNAP: - snap.end(); - break; - default: - Serial.printf("[ERROR] _pause: type not implemented: %d\n", current->type); - break; - } -} - -void Player::_stop() { - if (current != nullptr) { - switch (current->type) { - case T_SNAP: - snap.end(); - break; - default: - Serial.printf("[ERROR] _stop: type not implemented: %d\n", current->type); - break; - } - } - current = nullptr; - wanted = nullptr; -} - -// ------------------------------------------------------------------------------------------------ - -bool Player::_snapStart(const String &host) { - IPAddress ip; - WiFi.hostByName(host.c_str(), ip); - snap.setServerIP(ip); - return snap.begin(); -} diff --git a/src/Player.h b/src/Player.h deleted file mode 100644 index f941c64..0000000 --- a/src/Player.h +++ /dev/null @@ -1,86 +0,0 @@ -#ifndef PLAYER_H -#define PLAYER_H - -#include "Entry.h" - -#include - -#include "SnapClient.h" -#include "AudioTools/AudioCodecs/CodecOpus.h" -#include "AudioTools/AudioLibs/AudioBoardStream.h" - -enum State { - STOP, PLAY, PAUSE -}; - -class Player { - - WiFiClient wifi; - - AudioBoardStream board; - - OpusAudioDecoder opus; - - SnapClient snap; - - String playlistPath; - - size_t playlistSize = 0; - - bool playlistRequest = false; - - Entry *entries = nullptr; - - Entry *wanted = nullptr; - - Entry *current = nullptr; - - State should = STOP; - - bool repeatAll = false; - - bool repeatOne = false; - - bool shuffle = false; - -public: - - Player(); - - void loop(); - - void loadPlaylist(const String &path); - - void play(); - - void pause(); - - void stop(); - - void skip(int count); - - void next(); - - void previous(); - - void playIndex(size_t index); - -private: - - void _loadPlaylist(); - - void _start(); - - bool _isPlaying(); - - void _pause(); - - void _stop(); - - // ---------------------------------- - - bool _snapStart(const String &host); - -}; - -#endif diff --git a/src/audio.cpp b/src/audio.cpp index 4de624d..f2eddbc 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -1,76 +1,176 @@ #include "audio.h" -#include "SnapClient.h" -#include "wifi.h" -#include "AudioTools/AudioCodecs/CodecOpus.h" +#include "AudioTools.h" #include "AudioTools/AudioLibs/AudioBoardStream.h" +#include "AudioTools/AudioCodecs/CodecMP3Helix.h" +#include "AudioTools/AudioCodecs/CodecOpus.h" -WiFiClient wifi; +#include +#include + +#include "SnapClient.h" AudioBoardStream board(AudioKitEs8388V1); -OpusAudioDecoder opus; +File file; -SnapClient snap(wifi, board, opus); +ICYStream icy; -unsigned long snapLast = 0; +WiFiClient wifi; -bool snapState = false; +OpusAudioDecoder opusDecoder; -void audioSetup() { - board.setVolume(0.1); - snap.setServerIP(IPAddress(10, 0, 0, 50)); -} +SnapClient snap(wifi, board, opusDecoder); -void audioStarted() { - Serial.printf("SNAP started\n"); -} +MP3DecoderHelix mp3Decoder; -void audioStopped() { - Serial.printf("SNAP stopped\n"); - snapState = false; +EncodedAudioStream mp3Stream(&board, &mp3Decoder); + +StreamCopy copier; + +bool running = false; + +void audioStop() { + if (running) { + running = false; + Serial.println("[STOP]"); + } + copier.end(); + mp3Stream.end(); + mp3Decoder.end(); + opusDecoder.end(); + icy.end(); + file.close(); snap.end(); } -void audioStart() { - if (snapLast == 0 || millis() - snapLast >= 3000) { - Serial.printf("SNAP connecting...\n"); - snapLast = max(1UL, millis()); - snapState = snap.begin(); - if (snapState) { - Serial.printf("SNAP connected\n"); - } else { - Serial.printf("SNAP failed to connected\n"); - } +bool audioBeginMP3Stream(const char *code, Stream &source) { + if (!mp3Decoder.begin()) { + Serial.printf("[%s] Failed to start MP3DecoderHelix.", code); + audioStop(); + return false; } + + if (!mp3Stream.begin()) { + Serial.printf("[%s] Failed to start EncodedAudioStream.", code); + audioStop(); + return false; + } + + copier.begin(mp3Stream, source); + + if (copier.copy() == 0) { + Serial.printf("[%s] Failed to copy initial data.", code); + audioStop(); + return false; + } + + return true; } -void audioLoop() { - if (!isWifiConnected()) { - snapLast = 0; - if (snapState) { - audioStopped(); - } - return; +bool audioPlayICY(const String &url) { + audioStop(); + + Serial.printf("[ICY] %s", url.c_str()); + running = true; + + if (!icy.begin(url.c_str())) { + Serial.println("[ICY] Failed to start ICYStream."); + audioStop(); + return false; } - auto state = false; - if (snapState) { - state = snap.doLoop(); - } - - if (snapState) { - if (state) { - // still running - } else { - audioStopped(); - } - } else { - if (state) { - audioStarted(); - } else { - audioStart(); - } - } - snapState = state; + return audioBeginMP3Stream("ICY", icy); +} + +bool audioPlaySNAP(const String &host) { + audioStop(); + + Serial.printf("[SNAP] %s\n", host.c_str()); + running = true; + + IPAddress ip; + if (!WiFiClass::hostByName(host.c_str(), ip)) { + Serial.println("[SNAP] Failed to resolve host."); + audioStop(); + return false; + } + + snap.setServerIP(ip); + if (!snap.begin()) { + Serial.println("[SNAP] Failed to connect."); + audioStop(); + return false; + } + + if (!opusDecoder.begin()) { + Serial.println("[SNAP] Failed to start OpusAudioDecoder."); + audioStop(); + return false; + } + + if (!snap.doLoop()) { + Serial.println("[SNAP] Failed to copy initial data."); + audioStop(); + return false; + } + + return true; +} + +bool audioPlaySD(const String &url) { + audioStop(); + + Serial.printf("[SD] %s\n", url.c_str()); + running = true; + + if (!SD.begin(PIN_AUDIO_KIT_SD_CARD_CS)) { + Serial.println("[SD] Failed to mount SD-card."); + audioStop(); + return false; + } + + if (!SD.exists(url.c_str())) { + Serial.println("[SD] File not found."); + audioStop(); + return false; + } + + file = SD.open(url.c_str(), FILE_READ); + if (!file) { + Serial.println("[SD] Failed to open file."); + audioStop(); + return false; + } + + return audioBeginMP3Stream("SD", file); +} + +bool audioPlay(const String &url) { + if (url.startsWith("http://") || url.startsWith("https://")) { + return audioPlayICY(url); + } + if (url.startsWith("sd://")) { + return audioPlaySD(url.substring(5)); + } + if (url.startsWith("snap://") < 0) { + return audioPlaySNAP(url.substring(7)); + } + Serial.printf("[ERROR] unknown protocol: %s\n", url.c_str()); + return false; +} + +void audioSetup() { + board.begin(); +} + +bool audioLoop() { + if (copier.copy() > 0) { + return true; + } + if (running) { + Serial.println("Stream ended!"); + audioStop(); + } + return false; } diff --git a/src/audio.h b/src/audio.h index aa88d45..83e7488 100644 --- a/src/audio.h +++ b/src/audio.h @@ -1,8 +1,14 @@ -#ifndef SNAP_H -#define SNAP_H +#ifndef AUDIO_H +#define AUDIO_H + +#include + +bool audioPlay(const String &url); + +void audioStop(); void audioSetup(); -void audioLoop(); +bool audioLoop(); #endif diff --git a/src/main.cpp b/src/main.cpp index a1dcc15..c172687 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,15 +1,15 @@ #include #include "wifi.h" -#include "audio.h" +#include "player.h" void setup() { delay(500); Serial.begin(115200); - audioSetup(); + playerSetup(); } void loop() { wifiLoop(); - audioLoop(); + playerLoop(); } diff --git a/src/player.cpp b/src/player.cpp new file mode 100644 index 0000000..4c45674 --- /dev/null +++ b/src/player.cpp @@ -0,0 +1,42 @@ +#include "player.h" + +#include + +#include "audio.h" + +#define DELAY_MS_ADD 250UL +#define DELAY_MS_MAX 5000UL + +unsigned long errorMs = 0; + +unsigned long delayMs = 0; + +void playerSetup() { + audioSetup(); +} + +void playerLoop() { + if (audioLoop()) { + errorMs = 0; + return; + } + if (errorMs > 0 && millis() - errorMs < delayMs) { + return; + } + playerNext(); +} + +void playerNext() { + playerPlay("http://liveradio.sr.de/sr/sr1/mp3/128/stream.mp3"); +} + +void playerPlay(const String &url) { + if (audioPlay(url)) { + errorMs = 0; + delayMs = 0; + } else { + errorMs = max(1UL, millis()); + delayMs = min(DELAY_MS_MAX, delayMs + DELAY_MS_ADD); + Serial.printf("retry delay: %d ms\n", delayMs); + } +} diff --git a/src/player.h b/src/player.h new file mode 100644 index 0000000..cb73716 --- /dev/null +++ b/src/player.h @@ -0,0 +1,14 @@ +#ifndef PLAYER_H +#define PLAYER_H + +#include + +void playerNext(); + +void playerPlay(const String &url); + +void playerSetup(); + +void playerLoop(); + +#endif