commit b2f7b9cc79a5e8eddf26e3cd967c5d259ffb2655 Author: Patrick Haßel Date: Thu Jun 5 08:34:04 2025 +0200 wifi, Player, Playlist, SNAP diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..938b6ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.pio/ +/.idea/ diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..e792ec7 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,9 @@ +[env:Radio] +platform = espressif32 +board = esp32dev +framework = arduino +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 diff --git a/src/Entry.h b/src/Entry.h new file mode 100644 index 0000000..2c7e557 --- /dev/null +++ b/src/Entry.h @@ -0,0 +1,37 @@ +#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 new file mode 100644 index 0000000..367a3dc --- /dev/null +++ b/src/Player.cpp @@ -0,0 +1,222 @@ +#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 new file mode 100644 index 0000000..f941c64 --- /dev/null +++ b/src/Player.h @@ -0,0 +1,86 @@ +#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 new file mode 100644 index 0000000..4de624d --- /dev/null +++ b/src/audio.cpp @@ -0,0 +1,76 @@ +#include "audio.h" + +#include "SnapClient.h" +#include "wifi.h" +#include "AudioTools/AudioCodecs/CodecOpus.h" +#include "AudioTools/AudioLibs/AudioBoardStream.h" + +WiFiClient wifi; + +AudioBoardStream board(AudioKitEs8388V1); + +OpusAudioDecoder opus; + +SnapClient snap(wifi, board, opus); + +unsigned long snapLast = 0; + +bool snapState = false; + +void audioSetup() { + board.setVolume(0.1); + snap.setServerIP(IPAddress(10, 0, 0, 50)); +} + +void audioStarted() { + Serial.printf("SNAP started\n"); +} + +void audioStopped() { + Serial.printf("SNAP stopped\n"); + snapState = false; + 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"); + } + } +} + +void audioLoop() { + if (!isWifiConnected()) { + snapLast = 0; + if (snapState) { + audioStopped(); + } + return; + } + + auto state = false; + if (snapState) { + state = snap.doLoop(); + } + + if (snapState) { + if (state) { + // still running + } else { + audioStopped(); + } + } else { + if (state) { + audioStarted(); + } else { + audioStart(); + } + } + snapState = state; +} diff --git a/src/audio.h b/src/audio.h new file mode 100644 index 0000000..aa88d45 --- /dev/null +++ b/src/audio.h @@ -0,0 +1,8 @@ +#ifndef SNAP_H +#define SNAP_H + +void audioSetup(); + +void audioLoop(); + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..a1dcc15 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,15 @@ +#include + +#include "wifi.h" +#include "audio.h" + +void setup() { + delay(500); + Serial.begin(115200); + audioSetup(); +} + +void loop() { + wifiLoop(); + audioLoop(); +} diff --git a/src/wifi.cpp b/src/wifi.cpp new file mode 100644 index 0000000..cbbb7cc --- /dev/null +++ b/src/wifi.cpp @@ -0,0 +1,37 @@ +#include "wifi.h" + +#include + +unsigned long wifiLastMillis = 0; + +bool wifiConnected = false; + +void wifiLoop() { + const auto connected = WiFi.localIP() != 0; + if (wifiConnected) { + if (connected) { + // still connected + } else { + Serial.printf("WIFI disconnected!\n"); + } + } else { + if (connected) { + wifiLastMillis = 0; + Serial.printf("WIFI connected: %s\n", WiFi.localIP().toString().c_str()); + } else if (wifiLastMillis == 0 || millis() - wifiLastMillis > 10000) { + if (wifiLastMillis != 0) { + Serial.printf("WIFI connect timeout!\n"); + } + wifiLastMillis = max(1UL, millis()); + Serial.printf("WIFI connecting: %s\n", WiFi.SSID().c_str()); + WiFi.begin("HappyNet", "1Grausame!Sackratte7"); + } else { + // connecting + } + } + wifiConnected = connected; +} + +bool isWifiConnected() { + return wifiConnected; +} diff --git a/src/wifi.h b/src/wifi.h new file mode 100644 index 0000000..bb4aeb5 --- /dev/null +++ b/src/wifi.h @@ -0,0 +1,8 @@ +#ifndef WIFI_H +#define WIFI_H + +void wifiLoop(); + +bool isWifiConnected(); + +#endif