wifi, Player, Playlist, SNAP
This commit is contained in:
commit
b2f7b9cc79
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/.pio/
|
||||||
|
/.idea/
|
||||||
9
platformio.ini
Normal file
9
platformio.ini
Normal file
@ -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
|
||||||
37
src/Entry.h
Normal file
37
src/Entry.h
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#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
Normal file
222
src/Player.cpp
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
#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
Normal file
86
src/Player.h
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
#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
|
||||||
76
src/audio.cpp
Normal file
76
src/audio.cpp
Normal file
@ -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;
|
||||||
|
}
|
||||||
8
src/audio.h
Normal file
8
src/audio.h
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#ifndef SNAP_H
|
||||||
|
#define SNAP_H
|
||||||
|
|
||||||
|
void audioSetup();
|
||||||
|
|
||||||
|
void audioLoop();
|
||||||
|
|
||||||
|
#endif
|
||||||
15
src/main.cpp
Normal file
15
src/main.cpp
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#include "wifi.h"
|
||||||
|
#include "audio.h"
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
delay(500);
|
||||||
|
Serial.begin(115200);
|
||||||
|
audioSetup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
wifiLoop();
|
||||||
|
audioLoop();
|
||||||
|
}
|
||||||
37
src/wifi.cpp
Normal file
37
src/wifi.cpp
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#include "wifi.h"
|
||||||
|
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
8
src/wifi.h
Normal file
8
src/wifi.h
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#ifndef WIFI_H
|
||||||
|
#define WIFI_H
|
||||||
|
|
||||||
|
void wifiLoop();
|
||||||
|
|
||||||
|
bool isWifiConnected();
|
||||||
|
|
||||||
|
#endif
|
||||||
Loading…
Reference in New Issue
Block a user