fresh start including: ICY, SD, SNAP
This commit is contained in:
parent
b2f7b9cc79
commit
220f959e51
4
partitions-4m-factory.csv
Normal file
4
partitions-4m-factory.csv
Normal 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
|
||||
|
@ -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/bblanchon/ArduinoJson
|
||||
https://github.com/pschatzmann/arduino-libhelix
|
||||
37
src/Entry.h
37
src/Entry.h
@ -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
|
||||
222
src/Player.cpp
222
src/Player.cpp
@ -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();
|
||||
}
|
||||
86
src/Player.h
86
src/Player.h
@ -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
|
||||
208
src/audio.cpp
208
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 <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;
|
||||
}
|
||||
|
||||
12
src/audio.h
12
src/audio.h
@ -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
|
||||
|
||||
@ -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
42
src/player.cpp
Normal 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
14
src/player.h
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user