Merge branch 'cursor/igiRadio-ios-app' into HEAD

This commit is contained in:
2026-08-25 18:37:28 +02:00
3 changed files with 142 additions and 5 deletions
+10 -2
View File
@@ -82,12 +82,20 @@ BiquadCoefficients designPeakingEq(FrequencyHz center, GainDb gain,
const float a1 = -2.0F * cosOmega;
const float a2 = 1.0F - alpha / a;
// RBJ's cookbook form subtracts the feedback terms
// (y = ... - (a1/a0) y[n-1] - (a2/a0) y[n-2]); the ADAU1701 Param EQ
// cell's A0/A1 registers add them instead (y = ... + A0 y[n-1] +
// A1 y[n-2]), so the normalized RBJ a1/a2 must be negated when they
// land in A0/A1. Without this, any nonzero gain applies positive
// instead of negative feedback at the band's pole, so the biquad's
// state diverges and rails to a constant (inaudible DC) value --
// root cause of the 2026-08-25 total-silence-on-any-EQ-change bug.
return BiquadCoefficients{
.b0 = b0 / a0,
.b1 = b1 / a0,
.b2 = b2 / a0,
.a0 = a1 / a0,
.a1 = a2 / a0,
.a0 = -(a1 / a0),
.a1 = -(a2 / a0),
};
}
@@ -54,6 +54,55 @@ namespace {
return EXIT_SUCCESS;
}
/*
* The ADAU1701 Param EQ cell's A0/A1 registers ADD the feedback terms
* (y = ... + A0 y[n-1] + A1 y[n-2]), unlike the RBJ cookbook's native
* subtractive form. Denominator 1 - A0 z^-1 - A1 z^-2 = 0 has poles at the
* roots of z^2 - A0 z - A1 = 0; by the Jury test for a monic real quadratic
* z^2 + c1 z + c0 (c1 = -A0, c0 = -A1), both poles lie strictly inside the
* unit circle iff |A1| < 1, A0 + A1 < 1, and A0 - A1 > -1. A missing
* negation when mapping the RBJ a1/a2 into A0/A1 (2026-08-25 total-silence
* regression) fails this for real gain/Q combinations, so this guards
* against that class of bug rather than just checking magnitudes.
*/
[[nodiscard]] bool isAdau1701Stable(const core::BiquadCoefficients &c) noexcept
{
return std::fabs(c.a1) < 1.0F && (c.a0 + c.a1) < 1.0F
&& (c.a0 - c.a1) > -1.0F;
}
[[nodiscard]] int runPeakingStabilityTest()
{
const struct
{
std::uint32_t hz;
float dbGain;
float q;
} cases[] = {
{100U, 9.0F, 0.9F}, // bass-enhance band 1 at max level
{400U, 3.0F, 1.0F}, // bass-enhance band 2 at max level
{1000U, -1.5F, 1.0F}, // stereo-enhance band 3 at max level
{3000U, 2.0F, 1.0F}, // stereo-enhance band 4 at max level
{8000U, 4.0F, 1.0F}, // stereo-enhance band 5 at max level
{1000U, 12.0F, 10.0F}, // GainDb::kMaxDb at high Q
{1000U, -96.0F, 0.2F}, // near GainDb::kMinDb at low Q
};
for (const auto &tc : cases) {
const auto center = core::FrequencyHz::tryFromHz(tc.hz);
const auto gain = core::GainDb::tryFromDb(tc.dbGain);
const core::BiquadCoefficients peaking =
core::designPeakingEq(*center, *gain, tc.q);
if (!isAdau1701Stable(peaking)) {
std::cerr << "unstable ADAU1701 biquad for " << tc.hz << " Hz, "
<< tc.dbGain << " dB, Q=" << tc.q << ": a0=" << peaking.a0
<< " a1=" << peaking.a1 << '\n';
return EXIT_FAILURE;
}
}
return EXIT_SUCCESS;
}
} // namespace
int main()
@@ -64,5 +113,8 @@ int main()
if (runFlatBiquadTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
if (runPeakingStabilityTest() != EXIT_SUCCESS) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
@@ -494,8 +494,19 @@ void SigmaStudioTcpServer::stop() noexcept
vTaskDelete(task_);
task_ = nullptr;
}
activeListenFd().store(-1, std::memory_order_release);
if (listenFd_ >= 0) {
// Only clear the singleton if it still points at *our* fd: every
// boot path constructs this as a named local, start()s it, then
// moves it into NetBootstrap, so the moved-from local's own
// destructor runs stop() right after. An unconditional
// activeListenFd().store(-1) here used to stomp the atomic the
// moved-to (real, running) instance had just inherited, making
// acceptLoopTask() spin on accept(-1, ...) == EBADF forever from
// the very first boot -- root cause of the 2026-08-25 field
// observation, not a Wi-Fi-layer event.
int expected = listenFd_;
activeListenFd().compare_exchange_strong(expected, -1,
std::memory_order_acq_rel);
close(listenFd_);
listenFd_ = -1;
}
@@ -547,6 +558,59 @@ std::expected<void, NetError> SigmaStudioTcpServer::start()
return {};
}
namespace {
/**
* @brief recreateListenSocket — rebind a fresh listening socket on kPort.
*
* @dname recreateListenSocket
* @return The new fd on success (also stored in activeListenFd()), or -1.
* @pubstate closes the previous fd read from activeListenFd() if any, then
* publishes the new one.
*
* Self-healing counterpart to SigmaStudioTcpServer::start()'s socket setup.
* The 2026-08-25 field observation (accept() spinning on errno=EBADF
* forever) turned out to be a stop() lifetime bug, now fixed there: this
* function is kept as a safety net in case the singleton is ever cleared
* from underneath a running accept task by some future code path, not
* because it is expected to fire in normal operation.
*/
[[nodiscard]] int recreateListenSocket() noexcept
{
const int oldFd = activeListenFd().exchange(-1, std::memory_order_acq_rel);
if (oldFd >= 0) {
close(oldFd);
}
const int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (fd < 0) {
ESP_LOGE(kTag, "recreateListenSocket: socket() failed");
return -1;
}
const int reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(kPort);
if (bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0
|| listen(fd, 1) != 0) {
ESP_LOGE(kTag, "recreateListenSocket: bind/listen failed, errno=%d",
errno);
close(fd);
return -1;
}
activeListenFd().store(fd, std::memory_order_release);
ESP_LOGW(kTag, "SigmaStudio TCP listen socket recreated after failure");
return fd;
}
} // namespace
void SigmaStudioTcpServer::acceptLoopTask(void* /*arg*/)
{
while (true) {
@@ -556,8 +620,21 @@ void SigmaStudioTcpServer::acceptLoopTask(void* /*arg*/)
const int clientFd = accept(
listenFd, reinterpret_cast<sockaddr*>(&clientAddr), &clientLen);
if (clientFd < 0) {
ESP_LOGW(kTag, "accept() failed: errno=%d", errno);
vTaskDelay(pdMS_TO_TICKS(100));
// EBADF means the listen socket itself is gone -- retrying
// accept() on the same fd forever can never recover from this,
// unlike a transient per-call error, so rebuild the socket
// instead of just backing off and looping.
if (errno == EBADF) {
ESP_LOGE(kTag,
"accept() failed: listen socket invalid (errno=%d) "
"-- recreating",
errno);
(void)recreateListenSocket();
vTaskDelay(pdMS_TO_TICKS(500));
} else {
ESP_LOGW(kTag, "accept() failed: errno=%d", errno);
vTaskDelay(pdMS_TO_TICKS(100));
}
continue;
}
ESP_LOGI(kTag, "SigmaStudio client connected");