Loading files
/**
* @file surface/control_loop.cpp
* @brief alchemy::ControlLoop implementation.
*/
#include "alchemy/surface/control_loop.h"
#include "alchemy/hw/alchemy_lab.h"
#include "alchemy/surface/button_bank.h"
#include "alchemy/surface/settings.h"
#include "daisy_seed.h"
namespace alchemy {
ControlLoop::ControlLoop(AlchemyLab& hw, uint32_t frame_ms, uint32_t poll_ms)
: hw_(&hw),
frame_ms_(frame_ms),
poll_ms_(poll_ms ? poll_ms : 1u),
renderer_(hw.Arc())
{
}
ControlLoop& ControlLoop::Use(ButtonBank& bank)
{
buttons_ = &bank;
for (uint8_t i = 0; i < kNumButtons; i++) btn_refs_[i] = &hw_->buttons[i];
bank.Attach(btn_refs_, kNumButtons);
bank.AttachPageSource(this);
return *this;
}
void ControlLoop::WireKnobs()
{
/* Re-attach storage/locks/cv to every knob in every page. Called from
* Tick() so runtime changes (knobs added/removed/moved between pages
* by user code) are picked up on the next frame. */
for (uint8_t i = 0; i < num_pages_; i++)
{
Page* p = pages_[i];
const uint8_t idx = p->Index();
for (uint8_t j = 0; j < p->Count(); j++)
{
VirtualKnob* k = p->At(j);
if (k) WireKnob(*k, idx);
}
}
}
void ControlLoop::Tick()
{
/* Re-wire every frame so dynamic Page::Add/Remove takes effect. Cheap
* — just three pointer assigns per knob — and removes a class of bugs
* where a knob moves between pages but the loop still has stale refs. */
WireKnobs();
if (buttons_) buttons_->AttachStorage(storage_);
const uint32_t now = daisy::System::GetNow();
for (uint32_t elapsed = 0u; elapsed < frame_ms_; elapsed += poll_ms_)
{
hw_->ProcessAllControls();
const uint32_t t_ms = now + elapsed;
for (uint8_t i = 0; i < num_cv_; i++) cv_[i] = hw_->cv[i].Value();
// Initialize after hw.Init(); share one timestamp across this poll.
const uint32_t raw_tick = daisy::System::GetTick();
if (!poll_timebase_.Initialized())
poll_timebase_.Init(daisy::System::GetTickFreq(), raw_tick);
const uint32_t t_us = poll_timebase_.Update(raw_tick);
if (settings_) settings_->PollButtons(t_ms);
const bool gated = settings_ && settings_->IsActive();
if (cv_source_) cv_source_->PollEdges(cv_, t_us);
if (locks_) locks_ ->PollButtons(t_ms, gated);
if (buttons_)
buttons_->PollButtons(t_ms, gated,
storage_ ? storage_->ActivePage() : 0u);
if (storage_) storage_->PollButtons(t_ms, gated);
if (host_service_) host_service_->Poll(t_ms);
if (clip_) clip_->Tick(static_cast<float>(poll_ms_));
if (on_poll_) on_poll_(t_ms);
daisy::System::Delay(poll_ms_);
}
/* ── Snapshot physical pot positions for this frame. */
for (uint8_t p = 0; p < num_pots_; p++) phys_[p] = hw_->pots[p].Value();
/* ── Settings runs every frame so the B2+B3 enter gesture is always live. */
if (settings_) settings_->Update(phys_, now);
const bool in_settings = settings_ && settings_->IsActive();
/* ── Lock playback advances every frame, unconditionally: recorded
* modulation must keep running while Settings owns the surface.
* Only the gesture half (locks_->Update below) stands down. */
if (locks_)
{
locks_->SetFrameMs(frame_ms_);
locks_->Advance();
}
/* Deferred button-change callbacks run every frame, unconditionally:
* a host preset push must reach the app's Bind targets even while
* Settings owns the surface (data application is not a gesture). */
if (buttons_) buttons_->Update(now);
/* CV dispatch likewise runs unconditionally: patched CV keeps
* modulating the app while Settings owns the surface. */
if (cv_source_) cv_source_->Update(cv_, now);
if (!in_settings)
{
if (locks_) locks_ ->Update(phys_, now);
if (storage_) storage_->Update(phys_, now);
/* ── Page-change detection. */
if (storage_)
{
const uint8_t p = storage_->ActivePage();
if (p != last_page_)
{
last_page_ = p;
if (on_page_change_) on_page_change_();
}
}
/* ── User frame hook (typical: recompute DSP coefficients). */
if (on_frame_) on_frame_();
}
/* ── Render. */
Render(now);
}
void ControlLoop::Render(uint32_t t_ms)
{
hw_->leds.Clear();
/* Settings owns the screen while active. */
if (settings_ && settings_->IsActive())
{
settings_->Render(t_ms);
if (on_render_) on_render_(t_ms);
hw_->leds.Show();
return;
}
if (num_pages_ > 0)
{
/* Without storage, single-page modules render Page(0) always. */
const uint8_t active = storage_ ? storage_->ActivePage() : 0;
Page* page = nullptr;
for (uint8_t i = 0; i < num_pages_; i++)
if (pages_[i]->Index() == active) { page = pages_[i]; break; }
if (page)
{
/* Build pot-indexed arrays for PerfRenderer. */
ParamSlot slots [ControlLoop::kMaxPagePots] = {};
PotState pots [ControlLoop::kMaxPagePots] = {};
float combined[ControlLoop::kMaxPagePots] = {};
const uint8_t n = num_pots_ < ControlLoop::kMaxPagePots
? num_pots_
: ControlLoop::kMaxPagePots;
for (uint8_t p = 0; p < n; p++)
{
pots[p] = storage_ ? storage_->State(active, p)
: PotState{phys_[p], true, 0};
combined[p] = pots[p].stored;
}
for (uint8_t i = 0; i < page->Count(); i++)
{
VirtualKnob* k = page->At(i);
if (!k) continue;
const uint8_t p = k->Pot();
if (p >= n) continue;
slots[p] = k->Slot();
combined[p] = k->Norm();
}
renderer_.Render(hw_->leds, slots, pots, combined, n, t_ms);
/* Custom-ring + overdraw callbacks on this page's knobs. */
for (uint8_t i = 0; i < page->Count(); i++)
{
VirtualKnob* k = page->At(i);
if (k && k->HasCustomRing())
k->CustomFn()(hw_->leds, k->Pot(), hw_->Arc(),
k->Norm(), t_ms, k->CustomCtx());
}
for (uint8_t i = 0; i < page->Count(); i++)
{
VirtualKnob* k = page->At(i);
if (k && k->HasOverdraw())
k->OverdrawFn()(hw_->leds, k->Pot(), hw_->Arc(),
k->Norm(), t_ms, k->OverdrawCtx());
}
}
/* Navigation indicators: every binding paints its own button. */
if (storage_)
{
for (uint8_t i = 0; i < kNumButtons; i++)
{
const LedPanel::Rgb c =
storage_->IndicatorColor(&hw_->buttons[i]);
if (c.r || c.g || c.b) hw_->leds.SetButtonPair(i, c);
}
}
}
/* Button-state colors paint after the page indicator: a declared
* mode color deliberately wins over the page tint. */
if (buttons_)
buttons_->Render(hw_->leds,
storage_ ? storage_->ActivePage() : 0u, t_ms);
if (clip_) clip_->Draw(hw_->leds);
/* Global OnRender hook fires before the LockSource overlay so the
* overlay always sits on top of anything the user paints. */
if (on_render_) on_render_(t_ms);
if (locks_) locks_->Render(hw_->leds, t_ms);
hw_->leds.Show();
}
} // namespace alchemy
3f40b6632This file is pinned to the selected release. Download the original to open it in your design tool.