Add schedule sync and schedule-aware playback
This commit is contained in:
+85
-7
@@ -5,6 +5,7 @@
|
||||
#include "logical_size.h"
|
||||
#include "proclamation_options_page.h"
|
||||
#include "scramble/fibonacci_scrambler.h"
|
||||
#include "timezone_helper.h"
|
||||
#include "utf8_utils.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
@@ -72,6 +73,7 @@ void ScrollerPage::onPush(AppState &state) {
|
||||
createFont(state);
|
||||
createPatterns();
|
||||
|
||||
lines_.clear();
|
||||
prefilling_ = true;
|
||||
prefillLines();
|
||||
prefilling_ = false;
|
||||
@@ -79,9 +81,10 @@ void ScrollerPage::onPush(AppState &state) {
|
||||
// Start audio. In dynamic mode the current stream's solfeggio-frequency
|
||||
// track wins (when its file exists); otherwise the configured manual track
|
||||
// plays. A frequency file that is not on disk yet falls back to the manual
|
||||
// track rather than playing nothing.
|
||||
// track rather than playing nothing. A schedule block with dynamicAudio=true
|
||||
// forces dynamic audio for its duration; false leaves the device setting be.
|
||||
std::string track = state.config.get().audio.current_track;
|
||||
if (state.config.get().audio.dynamic_audio && !streams_.empty()) {
|
||||
if (effectiveDynamicAudio() && !streams_.empty()) {
|
||||
const ProclamationStream &stream = streams_[streamIndex_];
|
||||
if (stream.solfeggio_frequency_id != 0 &&
|
||||
!stream.audioFilename.empty() &&
|
||||
@@ -137,6 +140,14 @@ void ScrollerPage::update(float dt) {
|
||||
state_->audio.update(dt);
|
||||
}
|
||||
|
||||
// Block boundaries are minute-granular, so a one-second poll is plenty and
|
||||
// avoids mutating the process timezone every frame.
|
||||
schedulePollTimer_ -= dt;
|
||||
if (schedulePollTimer_ <= 0.0f) {
|
||||
schedulePollTimer_ = 1.0f;
|
||||
pollScheduleChange();
|
||||
}
|
||||
|
||||
updateAutoVelocity(dt);
|
||||
|
||||
scrollOffset_ += velocity_ * dt;
|
||||
@@ -200,8 +211,18 @@ void ScrollerPage::render(SDL_Renderer *renderer) {
|
||||
void ScrollerPage::buildStreams(AppState &state) {
|
||||
streams_.clear();
|
||||
fallbackPalette_.clear();
|
||||
lines_.clear();
|
||||
streamIndex_ = 0;
|
||||
scheduleChangePending_ = false;
|
||||
|
||||
// Schedule state: only an active schedule with a currently-matching block
|
||||
// changes the play list. The block boundary is handled in advanceStream(),
|
||||
// so rebuilding mid-playback never truncates the current proclamation.
|
||||
const radiantwave::config::AppConfig cfg = state.config.get();
|
||||
scheduleActive_ = cfg.proclamation.schedule_active;
|
||||
timezone_ = cfg.timezone;
|
||||
scheduleBlocks_ = scheduleActive_ ? state.db.getScheduleBlocks()
|
||||
: std::vector<radiantwave::db::ScheduleBlockRecord>{};
|
||||
activeBlockIndex_ = currentScheduleBlockIndex();
|
||||
|
||||
auto proclamations = state.db.getActiveProclamations();
|
||||
if (proclamations.empty()) {
|
||||
@@ -241,9 +262,53 @@ void ScrollerPage::buildStreams(AppState &state) {
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::string>* scheduledIds = nullptr;
|
||||
std::vector<std::string> scheduledOrder;
|
||||
if (activeBlockIndex_ >= 0 &&
|
||||
activeBlockIndex_ < static_cast<int>(scheduleBlocks_.size())) {
|
||||
scheduledOrder = scheduleBlocks_[activeBlockIndex_].proclamation_ids;
|
||||
scheduledIds = &scheduledOrder;
|
||||
}
|
||||
|
||||
streams_ = radiantwave::scroller::buildStreams(
|
||||
proclamations, palettesByFreqId, fallbackPalette_, audioByFreqId,
|
||||
DEFAULT_PROPLEAMATION_ID);
|
||||
DEFAULT_PROPLEAMATION_ID, scheduledIds);
|
||||
}
|
||||
|
||||
int ScrollerPage::currentScheduleBlockIndex() const {
|
||||
if (!scheduleActive_ || scheduleBlocks_.empty()) {
|
||||
return -1;
|
||||
}
|
||||
const int now = radiantwave::timezone::currentLocalMinutes(timezone_);
|
||||
for (size_t i = 0; i < scheduleBlocks_.size(); ++i) {
|
||||
const radiantwave::db::ScheduleBlockRecord &block = scheduleBlocks_[i];
|
||||
if (radiantwave::scroller::minuteInScheduleBlock(
|
||||
now, block.start_minutes, block.end_minutes, block.overnight)) {
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ScrollerPage::pollScheduleChange() {
|
||||
if (!scheduleActive_) {
|
||||
return;
|
||||
}
|
||||
if (currentScheduleBlockIndex() != activeBlockIndex_) {
|
||||
scheduleChangePending_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool ScrollerPage::effectiveDynamicAudio() const {
|
||||
// dynamicAudio=true on the active block forces dynamic audio, overriding
|
||||
// whatever the device selected. false means "don't override": the device's
|
||||
// own selection (dynamic or a fixed track) applies as usual.
|
||||
if (scheduleActive_ && activeBlockIndex_ >= 0 &&
|
||||
activeBlockIndex_ < static_cast<int>(scheduleBlocks_.size()) &&
|
||||
scheduleBlocks_[activeBlockIndex_].dynamic_audio) {
|
||||
return true;
|
||||
}
|
||||
return state_ ? state_->config.get().audio.dynamic_audio : false;
|
||||
}
|
||||
|
||||
void ScrollerPage::createFont(AppState &state) {
|
||||
@@ -451,7 +516,19 @@ void ScrollerPage::advanceStream() {
|
||||
if (streams_.empty()) {
|
||||
return;
|
||||
}
|
||||
streamIndex_ = (streamIndex_ + 1) % static_cast<int>(streams_.size());
|
||||
if (scheduleChangePending_ && state_) {
|
||||
// A schedule block changed while the previous proclamation was playing.
|
||||
// Apply it only now, at a stream boundary, so the current proclamation
|
||||
// finishes instead of being cut off.
|
||||
buildStreams(*state_);
|
||||
createPatterns();
|
||||
streamIndex_ = 0;
|
||||
} else {
|
||||
streamIndex_ = (streamIndex_ + 1) % static_cast<int>(streams_.size());
|
||||
}
|
||||
if (streams_.empty()) {
|
||||
return;
|
||||
}
|
||||
// Diagnostic: report the palette the incoming stream will render so the
|
||||
// console shows whether colors should change when this stream starts.
|
||||
const ProclamationStream &stream = streams_[streamIndex_];
|
||||
@@ -473,8 +550,9 @@ void ScrollerPage::switchAudioForCurrentStream() {
|
||||
}
|
||||
|
||||
// Dynamic audio only: with a fixed manual track selected the audio must
|
||||
// never switch as proclamations change.
|
||||
if (!state_->config.get().audio.dynamic_audio) {
|
||||
// never switch as proclamations change. An active schedule block's
|
||||
// dynamicAudio flag overrides the device-wide setting.
|
||||
if (!effectiveDynamicAudio()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user