fresh start including: ICY, SD, SNAP

This commit is contained in:
Patrick Haßel 2025-06-05 12:29:56 +02:00
parent b2f7b9cc79
commit 220f959e51
10 changed files with 229 additions and 406 deletions

View File

@ -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
1 # Name, Type, SubType, Offset, Size, Flags
2 nvs, data, nvs, 0x9000, 0x6000,
3 phy_init, data, phy, 0xf000, 0x1000,
4 factory, app, factory, 0x10000, 4M

View File

@ -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
https://github.com/pschatzmann/arduino-libhelix

View File

@ -1,37 +0,0 @@
#ifndef ENTRY_H
#define ENTRY_H
#include <ArduinoJson.h>
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

View File

@ -1,222 +0,0 @@
#include "Player.h"
#include <LittleFS.h>
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<JsonArray>();
playlistSize = jsonEntries.size();
if (playlistSize <= 0) {
Serial.printf("Loaded playlist is empty.");
return;
}
entries = static_cast<Entry *>(malloc(sizeof(Entry) * playlistSize));
wanted = entries;
Entry *entry = entries;
for (auto jsonVar: jsonEntries) {
auto jsonEntry = jsonVar.as<JsonObject>();
if (jsonEntry == NULL) {
Serial.printf("Entry #%03d is not a JsonObject\n", entry - entries);
continue;
}
if (!jsonEntry["type"].is<String>()) {
Serial.printf("Entry #%03d has no String 'type'\n", entry - entries);
continue;
}
if (!jsonEntry["url"].is<String>()) {
Serial.printf("Entry #%03d has no String 'url'\n", entry - entries);
continue;
}
if (!jsonEntry["title"].is<String>()) {
Serial.printf("Entry #%03d has no String 'title'\n", entry - entries);
continue;
}
entry->type = typeFromString(jsonEntry["type"].as<String>());
entry->url = jsonEntry["url"].as<String>();
entry->title = jsonEntry["title"].as<String>();
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();
}

View File

@ -1,86 +0,0 @@
#ifndef PLAYER_H
#define PLAYER_H
#include "Entry.h"
#include <WString.h>
#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

View File

@ -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 <SPI.h>
#include <SD.h>
#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;
}

View File

@ -1,8 +1,14 @@
#ifndef SNAP_H
#define SNAP_H
#ifndef AUDIO_H
#define AUDIO_H
#include <WString.h>
bool audioPlay(const String &url);
void audioStop();
void audioSetup();
void audioLoop();
bool audioLoop();
#endif

View File

@ -1,15 +1,15 @@
#include <Arduino.h>
#include "wifi.h"
#include "audio.h"
#include "player.h"
void setup() {
delay(500);
Serial.begin(115200);
audioSetup();
playerSetup();
}
void loop() {
wifiLoop();
audioLoop();
playerLoop();
}

42
src/player.cpp Normal file
View File

@ -0,0 +1,42 @@
#include "player.h"
#include <Arduino.h>
#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);
}
}

14
src/player.h Normal file
View File

@ -0,0 +1,14 @@
#ifndef PLAYER_H
#define PLAYER_H
#include <WString.h>
void playerNext();
void playerPlay(const String &url);
void playerSetup();
void playerLoop();
#endif