Files
project-vesper/vesper/src/Player/Player.cpp

476 lines
15 KiB
C++

#include "Player.hpp"
#include "../Communication/CommunicationRouter/CommunicationRouter.hpp"
#include "../BellEngine/BellEngine.hpp"
// Note: Removed global melody_steps dependency for cleaner architecture
// Constructor with dependencies
Player::Player(CommunicationRouter* comm, FileManager* fm)
: id(0)
, name("melody1")
, uid("x")
, url("-")
, noteAssignments{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}
, speed(500)
, segment_duration(15000)
, pause_duration(0)
, total_duration(0)
, segmentCmpltTime(0)
, segmentStartTime(0)
, startTime(0)
, pauseTime(0)
, continuous_loop(false)
, infinite_play(false)
, isPlaying(false)
, isPaused(false)
, hardStop(false)
, _status(PlayerStatus::STOPPED)
, _commManager(comm)
, _fileManager(fm)
, _bellEngine(nullptr)
, _durationTimerHandle(NULL) {
}
// Default constructor (for backward compatibility)
Player::Player()
: id(0)
, name("melody1")
, uid("x")
, url("-")
, noteAssignments{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}
, speed(500)
, segment_duration(15000)
, pause_duration(0)
, total_duration(0)
, segmentCmpltTime(0)
, segmentStartTime(0)
, startTime(0)
, pauseTime(0)
, continuous_loop(false)
, infinite_play(false)
, isPlaying(false)
, isPaused(false)
, hardStop(false)
, _status(PlayerStatus::STOPPED)
, _commManager(nullptr)
, _fileManager(nullptr)
, _bellEngine(nullptr)
, _durationTimerHandle(NULL) {
}
void Player::setDependencies(CommunicationRouter* comm, FileManager* fm) {
_commManager = comm;
_fileManager = fm;
}
// Destructor
Player::~Player() {
// Stop any ongoing playback
if (isPlaying) {
forceStop();
}
// Properly cleanup timer
if (_durationTimerHandle != NULL) {
xTimerDelete(_durationTimerHandle, portMAX_DELAY);
_durationTimerHandle = NULL;
}
}
void Player::begin() {
LOG_INFO("Initializing Player with FreeRTOS Timer (saves 4KB RAM!)");
// Create a periodic timer that fires every 500ms
_durationTimerHandle = xTimerCreate(
"PlayerTimer", // Timer name
pdMS_TO_TICKS(500), // Period (500ms)
pdTRUE, // Auto-reload (periodic)
this, // Timer ID (pass Player instance)
durationTimerCallback // Callback function
);
if (_durationTimerHandle != NULL) {
xTimerStart(_durationTimerHandle, 0);
LOG_INFO("Player initialized successfully with timer");
} else {
LOG_ERROR("Failed to create Player timer!");
}
}
void Player::play() {
if (_melodySteps.empty()) {
LOG_ERROR("Cannot play: No melody loaded");
return;
}
if (_bellEngine) {
_bellEngine->setMelodyData(_melodySteps);
_bellEngine->start();
}
isPlaying = true;
hardStop = false;
startTime = segmentStartTime = millis();
setStatus(PlayerStatus::PLAYING); // Update status and notify clients
LOG_DEBUG("Plbck: PLAY");
}
void Player::forceStop() {
if (_bellEngine) {
_bellEngine->emergencyStop();
}
hardStop = true;
isPlaying = false;
setStatus(PlayerStatus::STOPPED); // Immediate stop, notify clients
LOG_DEBUG("Plbck: FORCE STOP");
}
void Player::stop() {
if (_bellEngine) {
_bellEngine->stop();
}
hardStop = false;
isPlaying = false;
// Set STOPPING status - actual stop message will be sent when BellEngine finishes
setStatus(PlayerStatus::STOPPING);
LOG_DEBUG("Plbck: SOFT STOP (waiting for melody to complete)");
// NOTE: The actual "stop" message is now sent in onMelodyLoopCompleted()
// when the BellEngine actually finishes the current loop
}
void Player::pause() {
isPaused = true;
setStatus(PlayerStatus::PAUSED);
LOG_DEBUG("Plbck: PAUSE");
}
void Player::unpause() {
isPaused = false;
segmentStartTime = millis();
setStatus(PlayerStatus::PLAYING);
LOG_DEBUG("Plbck: RESUME");
}
bool Player::command(JsonVariant data) {
setMelodyAttributes(data);
loadMelodyInRAM(); // Removed parameter - use internal storage
String action = data["action"];
LOG_DEBUG("Incoming Command: %s", action.c_str());
// Play or Stop Logic
if (action == "play") {
play();
return true;
} else if (action == "stop") {
forceStop();
return true;
} else {
LOG_WARNING("Unknown playback action: %s", action.c_str());
return false;
}
}
void Player::setMelodyAttributes(JsonVariant doc) {
if (doc.containsKey("name")) {
name = doc["name"].as<const char*>();
}
if (doc.containsKey("uid")) {
uid = doc["uid"].as<const char*>();
}
if (doc.containsKey("url")) {
url = doc["url"].as<const char*>();
}
if (doc.containsKey("speed")) {
speed = doc["speed"].as<uint16_t>();
}
if (doc.containsKey("note_assignments")) {
JsonArray noteArray = doc["note_assignments"];
size_t arraySize = min(noteArray.size(), (size_t)16);
for (size_t i = 0; i < arraySize; i++) {
noteAssignments[i] = noteArray[i];
}
}
if (doc.containsKey("segment_duration")) {
segment_duration = doc["segment_duration"].as<uint32_t>();
}
if (doc.containsKey("pause_duration")) {
pause_duration = doc["pause_duration"].as<uint32_t>();
}
if (doc.containsKey("total_duration")) {
total_duration = doc["total_duration"].as<uint32_t>();
}
if (doc.containsKey("continuous_loop")) {
continuous_loop = doc["continuous_loop"].as<bool>();
}
if (continuous_loop && total_duration == 0) {
infinite_play = true;
}
if (!continuous_loop) {
total_duration = segment_duration;
}
// Print Just for Debugging Purposes
LOG_DEBUG("Set Melody Vars / Name: %s, UID: %s",
name.c_str(), uid.c_str());
LOG_DEBUG("URL: %s", url.c_str());
LOG_DEBUG("Speed: %d, Per Segment Duration: %lu, Pause Duration: %lu, Total Duration: %d, Continuous: %s, Infinite: %s",
speed, segment_duration, pause_duration, total_duration,
continuous_loop ? "true" : "false", infinite_play ? "true" : "false");
}
void Player::loadMelodyInRAM() {
String filePath = "/melodies/" + String(uid.c_str());
File bin_file = SD.open(filePath.c_str(), FILE_READ);
if (!bin_file) {
LOG_ERROR("Failed to open file: %s", filePath.c_str());
LOG_ERROR("Check Servers for the File...");
// Try to download the file using FileManager
if (_fileManager) {
StaticJsonDocument<128> doc;
doc["download_url"] = url;
doc["melodys_uid"] = uid;
if (!_fileManager->addMelody(doc)) {
LOG_ERROR("Failed to Download File. Check Internet Connection");
return;
} else {
bin_file = SD.open(filePath.c_str(), FILE_READ);
}
} else {
LOG_ERROR("FileManager not available for download");
return;
}
}
size_t fileSize = bin_file.size();
if (fileSize % 2 != 0) {
LOG_ERROR("Invalid file size: %u (not a multiple of 2)", fileSize);
bin_file.close();
return;
}
// Load into Player's internal melody storage only
_melodySteps.resize(fileSize / 2);
for (size_t i = 0; i < _melodySteps.size(); i++) {
uint8_t high = bin_file.read();
uint8_t low = bin_file.read();
_melodySteps[i] = (high << 8) | low;
}
LOG_INFO("Melody loaded successfully: %d steps", _melodySteps.size());
bin_file.close();
}
// Static timer callback function for FreeRTOS
void Player::durationTimerCallback(TimerHandle_t xTimer) {
// Get Player instance from timer ID
Player* player = static_cast<Player*>(pvTimerGetTimerID(xTimer));
// Only run checks when actually playing
if (!player->isPlaying) {
return;
}
unsigned long now = millis();
if (player->timeToStop(now)) {
player->stop();
} else if (player->timeToPause(now)) {
player->pause();
} else if (player->timeToResume(now)) {
player->unpause();
}
}
// Check if it's time to stop playback
bool Player::timeToStop(unsigned long now) {
if (isPlaying && !infinite_play && total_duration == 0) {
if (now > startTime){}
LOG_DEBUG("(Single Loop Run Seelected) Soft Stopping.");
return true;
}
}
else if (isPlaying && !infinite_play) {
uint64_t stopTime = startTime + total_duration;
if (now >= stopTime) {
LOG_DEBUG("(TimerFunction) Total Run Duration Reached. Soft Stopping.");
return true;
}
}
return false;
}
// Status management and BellEngine callback
void Player::setStatus(PlayerStatus newStatus) {
if (_status == newStatus) {
return; // No change, don't send duplicate messages
}
PlayerStatus oldStatus = _status;
_status = newStatus;
// Send appropriate message to ALL clients (WebSocket + MQTT) based on status change
if (_commManager) {
StaticJsonDocument<256> doc;
doc["status"] = "INFO";
doc["type"] = "playback";
// Create payload object for complex data
JsonObject payload = doc.createNestedObject("payload");
switch (newStatus) {
case PlayerStatus::PLAYING:
payload["action"] = "playing";
payload["time_elapsed"] = (millis() - startTime) / 1000; // Convert to seconds
break;
case PlayerStatus::PAUSED:
payload["action"] = "paused";
payload["time_elapsed"] = (millis() - startTime) / 1000;
break;
case PlayerStatus::STOPPED:
payload["action"] = "idle";
payload["time_elapsed"] = 0;
break;
case PlayerStatus::STOPPING:
payload["action"] = "stopping";
payload["time_elapsed"] = (millis() - startTime) / 1000;
break;
}
// Add projected run time for all states (0 if not applicable)
uint64_t projectedRunTime = calculateProjectedRunTime();
payload["projected_run_time"] = projectedRunTime;
// 🔥 Use broadcastStatus() to send to BOTH WebSocket AND MQTT clients!
_commManager->broadcastStatus(doc);
LOG_DEBUG("Status changed: %d → %d, broadcast sent with runTime: %llu",
(int)oldStatus, (int)newStatus, projectedRunTime);
}
}
void Player::onMelodyLoopCompleted() {
// This is called by BellEngine when a melody loop actually finishes
if (_status == PlayerStatus::STOPPING) {
// We were in soft stop mode, now actually stop
setStatus(PlayerStatus::STOPPED);
LOG_DEBUG("Plbck: ACTUAL STOP (melody loop completed)");
}
// Mark segment completion time
segmentCmpltTime = millis();
}
// Check if it's time to pause playback
bool Player::timeToPause(unsigned long now) {
if (isPlaying && continuous_loop) {
// Special case: segment_duration = 0 means "one loop only"
if (segment_duration == 0) {
// Only pause after first loop completes (segmentCmpltTime updated)
if (segmentCmpltTime > segmentStartTime && !isPaused) {
LOG_DEBUG("(TimerFunction) One-loop segment completed. Pausing.");
pauseTime = now;
return true;
}
} else {
// Normal duration-based pausing
uint64_t timeToPause = segmentStartTime + segment_duration;
LOG_DEBUG("PTL: %llu // NOW: %lu", timeToPause, now);
if (now >= timeToPause && !isPaused) {
LOG_DEBUG("(TimerFunction) Segment Duration Reached. Pausing.");
pauseTime = now;
return true;
}
}
}
return false;
}
// Check if it's time to resume playback
bool Player::timeToResume(unsigned long now) {
if (isPaused) {
uint64_t timeToResume = segmentCmpltTime + pause_duration;
if (now >= timeToResume) {
LOG_DEBUG("(TimerFunction) Pause Duration Reached. Resuming");
segmentStartTime = now; // Reset segment start time for next cycle
return true;
}
}
return false;
}
// Calculate the projected total run time of current playback
uint64_t Player::calculateProjectedRunTime() const {
if (_melodySteps.empty() || (_status == PlayerStatus::STOPPED)) {
return 0; // No melody loaded or actually stopped
}
// Calculate single loop duration: steps * speed (in milliseconds)
uint32_t singleLoopDuration = _melodySteps.size() * speed;
if (infinite_play || total_duration == 0) {
return 0; // Infinite playback has no end time
}
// Calculate how many loops are needed to meet or exceed total_duration
uint32_t loopsNeeded = (total_duration + singleLoopDuration - 1) / singleLoopDuration; // Ceiling division
// Calculate actual total duration (this is the projected run time)
uint32_t actualTotalDuration = singleLoopDuration * loopsNeeded;
// Return the total duration (offset from start), not a timestamp
return actualTotalDuration;
}
// ════════════════════════════════════════════════════════════════════════════
// HEALTH CHECK IMPLEMENTATION
// ════════════════════════════════════════════════════════════════════════════
bool Player::isHealthy() const {
// Check if dependencies are properly set
if (!_commManager) {
LOG_DEBUG("Player: Unhealthy - Communication manager not set");
return false;
}
if (!_fileManager) {
LOG_DEBUG("Player: Unhealthy - File manager not set");
return false;
}
if (!_bellEngine) {
LOG_DEBUG("Player: Unhealthy - BellEngine not set");
return false;
}
// Check if timer is properly created
if (_durationTimerHandle == NULL) {
LOG_DEBUG("Player: Unhealthy - Duration timer not created");
return false;
}
// Check if timer is actually running
if (xTimerIsTimerActive(_durationTimerHandle) == pdFALSE) {
LOG_DEBUG("Player: Unhealthy - Duration timer not active");
return false;
}
// Check for consistent playback state
if (isPlaying && (_status == PlayerStatus::STOPPED)) {
LOG_DEBUG("Player: Unhealthy - Inconsistent playback state");
return false;
}
return true;
}