Skip to content

[feature] Add animated community credits experience#4250

Open
jensenpat wants to merge 1 commit into
aethersdr:mainfrom
jensenpat:aether/community-credits-demo
Open

[feature] Add animated community credits experience#4250
jensenpat wants to merge 1 commit into
aethersdr:mainfrom
jensenpat:aether/community-credits-demo

Conversation

@jensenpat

@jensenpat jensenpat commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a memorable, demo-scene-inspired community credits experience to the About dialog and gives users a direct path to support AetherSDR.

The new Play Community Credits... action opens a dedicated, persistent dialog built entirely with Qt. It places animated contributor credits inside the LCD of a high-resolution FLEX-8600 cutout, surrounds the radio with a procedural starfield and aurora, and plays a bundled Scream Tracker track while the credits run.

AetherSDR Community Credits

What changed

  • Added a Play Community Credits... button to About.
  • Added a resizable PersistentDialog that follows the existing frameless-window and geometry-persistence behavior.
  • Isolated the complete dialog, renderer, supporter client, controls, and audio lifecycle in Contribute.{h,cpp}; MainWindow_Menus.cpp retains only the About button and its one-line launcher.
  • Added a CPU-rendered QPainter scene with:
    • procedural background stars and aurora;
    • a perspective star tunnel and copper-bar effects inside the radio display;
    • an animated title, flying supporter names, and a DXYCP-style text scroller;
    • a restrained static footer: DEFINING THE FRONTIER OF SOFTWARE-DEFINED RADIO.
  • Added a cleaned transparent PNG cutout of the FLEX-8600. The original aurora.jpg source is intentionally not part of this PR.
  • Added a Contribute button linking to the AetherSDR website's sponsor section, providing a stable app-facing URL if sponsorship providers change later.
  • Routed Help → Donate to AetherSDR through the same website sponsor section instead of linking directly to the current provider.
  • Added looping background music with Mute Music / Unmute Music controls and dialog-owned playback lifetime.
  • Added accessible names and descriptions for the credits action, canvas, music metadata, and controls.

Live supporter data

The dialog requests the 50 newest BACKER members from Open Collective's GraphQL v2 API when it opens. Results are deduplicated case-insensitively and incognito, empty, or guest entries are represented as Anonymous Signal Booster.

The network path is deliberately non-blocking:

  • the request is aborted after seven seconds;
  • the built-in generic contributor/tester/reporter/signal-booster scroller remains usable if the service is unavailable or returns no names;
  • no Open Collective response is persisted locally.

Cross-platform implementation

The visual effects use QWidget/QPainter and a 16 ms Qt timer, so this does not introduce a new rendering backend or require 3D acceleration. LCD coordinates are defined in source-image space and scaled into the current dialog geometry, keeping animation clipped to the physical display at different sizes and DPI settings.

Music playback uses QSoundEffect with a resource-embedded 11.025 kHz mono PCM WAV. The original S3M is retained alongside the rendered track for source/provenance, while WAV playback avoids adding a module-player dependency or relying on platform-specific compressed-audio codecs. The lower sample rate reinforces the tracker-era character and reduces the WAV from 7,057,544 bytes to 1,621,098 bytes (77%). No new third-party library is introduced.

Assets

  • resources/aurora-cutout.png — cleaned, alpha-transparent radio cutout used at runtime.
  • resources/c512w-friday.s3m — source Scream Tracker 3 module.
  • resources/community-credits.wav — 11.025 kHz mono, 16-bit PCM render used by Qt at runtime.
  • docs/screenshots/community-credits.png — current macOS visual reference.

resources/aurora.jpg is intentionally excluded.

Validation

  • Rebased onto current origin/main.
  • Built AetherSDR successfully on macOS with cmake --build build --target AetherSDR -j8.
  • Opened and visually inspected the feature in the built application.
  • Verified the About entry, extracted ContributeDialog, responsive radio/LCD composition, supporter scroller, music playback, mute/unmute toggle, close behavior, and Contribute control through the agent automation bridge.
  • Ran python3 tools/check_a11y.py; no findings were introduced by this change (the scanner reports eight pre-existing warnings in five unrelated files).
  • Ran python3 tools/check_engine_boundary.py --strict; no blocking findings.
  • Ran git diff origin/main...HEAD --check successfully.
  • Verified the commit is SSH-signed.
  • Verified the PR file set contains aurora-cutout.png and does not contain aurora.jpg.

Follow-up automation opportunities

The visual behavior is currently tested through app inspection. Useful bridge verbs for deterministic future coverage would be:

  • communityCredits.open
  • communityCredits.state
  • communityCredits.setMuted
  • communityCredits.capturePhase(ms)

👨🏼‍💻 Generated with OpenAI Codex (GPT-5.6 Sol) and tested by @jensenpat

@jensenpat jensenpat changed the title Add animated community credits experience [ux] Add animated community credits experience Jul 15, 2026
@jensenpat
jensenpat marked this pull request as ready for review July 15, 2026 03:14
@jensenpat
jensenpat requested review from a team as code owners July 15, 2026 03:14

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really fun contribution — the demo-scene LCD credits are a delightful touch, and the engineering is careful: the network path is context-scoped and non-blocking, JSON parsing degrades to the built-in scroller on any failure, accessibility names/descriptions are set throughout, and it reuses PersistentDialog/trackPersistentDialog correctly. Build, CodeQL, a11y, and both mac/win checks are green, and the commit is signed. No correctness blockers — my notes are about placement and one design decision.

Would like fixed before merge

  • ~575 lines of a self-contained widget + animation engine + network client are living in an anonymous namespace inside the menu-builder TU. Please lift it into its own CommunityCreditsDialog.{h,cpp} — see inline.

Non-blocking notes

  • Opening the dialog fires an unconditional POST to api.opencollective.com with no opt-out — worth a maintainer decision (inline).
  • resources/c512w-friday.s3m is committed but referenced nowhere (not in resources.qrc, not in code) — only the rendered .wav is used at runtime. If it's kept purely for provenance, consider dropping it from the tree or noting its purpose, rather than shipping an unreferenced binary blob under resources/.

Polish

  • The 16 ms animation timer spins whether or not the dialog is visible (inline).

🤖 aethersdr-agent · cost: $6.6713 · model: claude-opus-4-8

Comment thread src/gui/MainWindow_Menus.cpp Outdated
qreal drift{0.0};
};

class CommunityCreditsCanvas final : public QWidget {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole block — CommunityCreditsCanvas, the aurora/starfield/scroller renderer, and fetchOpenCollectiveSupporters — is a ~575-line subsystem sitting in an anonymous namespace inside the menu-builder translation unit. It has no dependency on MainWindow internals until the button wiring at the call site, so it would be much easier to maintain (and much cheaper to recompile) as its own src/gui/CommunityCreditsDialog.{h,cpp} exposing a small factory/open entry point. MainWindow_Menus.cpp would then just construct and show it. Not a correctness issue — the engine/UI boundary check already passes — but a sizable altitude/maintainability concern for a menu TU.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will implement

Comment thread src/gui/MainWindow_Menus.cpp Outdated
Comment thread src/gui/MainWindow_Menus.cpp Outdated

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for a genuinely fun, well-crafted PR — the QPainter scene is clean, lambdas are correctly scoped to owner so there are no use-after-free traps across the network round-trip, the 7 s abort timer and the graceful "fall back to the generic scroller on any failure" path are exactly right, and geometry persistence goes through PersistentDialog rather than raw QSettings. The C++ is solid.

My reservations are about weight and scope, not correctness. The two concrete things worth resolving before merge are the ~8.4 MB of binary assets baked into the application, and a meta question that's really a maintainer call: whether a demo-scene credits screen with looping music and a live Open Collective fetch fits the character of a FlexRadio workstation. I'm posting COMMENT — the merge decision is yours.

Would like discussed before merge

  • resources/community-credits.wav (6.9 MB) + aurora-cutout.png (1.5 MB) are embedded into every build via the qrc — ~8.4 MB added to the binary for one easter-egg dialog. See inline for options.
  • resources/c512w-friday.s3m (25 KB) is committed but is not in resources.qrc, so it never ships at runtime — confirm it's intentional source/provenance and not a leftover.

Non-blocking notes

  • The dialog fires an outbound request to api.opencollective.com every time it opens (see inline) — fine since it's user-initiated and non-blocking, just flagging the offline/privacy angle.
  • paintDxycpScroller names a local exit (shadows std::exit) — cosmetic, ignore if you like.

🤖 aethersdr-agent · cost: $3.5215 · model: claude-opus-4-8

Comment thread resources/resources.qrc
Comment on lines +17 to +20
<file alias="community-credits.wav">community-credits.wav</file>
</qresource>
<qresource prefix="/images">
<file alias="aurora-cutout.png">aurora-cutout.png</file>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two files embed ~8.4 MB into the AetherSDR binary itself (community-credits.wav is 6.9 MB, aurora-cutout.png is 1.5 MB), carried into every install for a single credits dialog.

Worth reconsidering: (a) a much smaller/compressed WAV — 0.08 volume looping background music doesn't need 24 kHz stereo PCM; even mono ~11 kHz would cut this by 3–4×; and/or (b) loading these from an on-disk resource dir instead of the qrc so they don't inflate the executable. If they stay in the qrc, please at least shrink the WAV — it's the bulk of the cost.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will implement

Comment thread src/gui/MainWindow_Menus.cpp Outdated
@jensenpat jensenpat changed the title [ux] Add animated community credits experience [feature] Add animated community credits experience Jul 16, 2026
@jensenpat
jensenpat force-pushed the aether/community-credits-demo branch 2 times, most recently from 66fac3b to 02569cf Compare July 16, 2026 15:57
@jensenpat jensenpat self-assigned this Jul 16, 2026
@jensenpat
jensenpat force-pushed the aether/community-credits-demo branch from 02569cf to 6f79923 Compare July 16, 2026 16:17

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good PR — clean isolation, the animation code is well-crafted, and the fallback path makes it fully usable offline. Two items worth fixing before merge, plus a couple of nits.

Medium — opacity(0.99) slow compositing path (inline comment on Contribute.cpp)

Medium — three unused includes left in MainWindow_Menus.cpp (inline comment on that file)

Minor — QSoundEffect silent failure has no user signal

QSoundEffect silently fails to open the audio device on some Linux PipeWire/ALSA configurations — play() returns but nothing plays. The dialog works fine muted, but the control bar still shows "Mute Music" when no music is playing. A one-line connect to QSoundEffect::statusChanged that hides or grays the track label and mute button on QSoundEffect::Error would make the failure visible. Could be a follow-up.

Nit — c512w-friday.s3m is dead weight at runtime

The S3M module is in resources/ for provenance but not embedded in resources.qrc — only the WAV is used at runtime. It travels in git history as a 65 KB binary blob that serves no build or runtime purpose. Consider moving it to docs/audio-sources/ or noting in a comment that it's source-only, so future contributors don't spend time wondering why it's there.

Nit — canvas raw pointer capture is safe but non-obvious

void fetchOpenCollectiveSupporters(QDialog* owner, CommunityCreditsCanvas* canvas)

The lambda captures canvas by raw pointer. It's safe — canvas is a child of owner and both are destroyed together, and the QNetworkReply is deleted by its manager when owner closes, so the lambda never fires on a dangling pointer. But it's subtle enough to be worth a QPointer + null-check or a brief comment documenting the lifetime guarantee.


Generated by Claude Code

Comment thread src/gui/Contribute.cpp
viewport.center().x() - sceneSize.width() / 2.0,
viewport.center().y() - sceneSize.height() / 2.0,
sceneSize.width(), sceneSize.height());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — slow compositing path for no visual gain

painter.setOpacity(0.99);
painter.drawImage(sceneRect, m_radioImage);
painter.setOpacity(1.0);

Any opacity value other than exactly 1.0 forces QPainter into a software compositing path — it reads back the destination pixels, blends, and writes, instead of blitting directly. On a 1040×680 dialog that's a ~4 MB read per frame at 60 fps.

The aurora is painted before the radio image and the PNG cutout has its own alpha channel where transparent, so the aurora already bleeds through the cutout at 1.0 opacity. Set this to 1.0 or add a comment explaining what the 0.99 is preserving that the alpha channel doesn't already give you.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — three leftover includes

+#include <QColor>
+#include <QJsonObject>
+#include <QNetworkRequest>

All network and JSON handling moved into Contribute.cpp as intended, but these three headers were left behind. None are used directly in this TU after the refactor — QColor and QJsonObject don't appear in the remaining code, and QNetworkRequest was only needed by the fetch helper that's now in Contribute.cpp. Remove them so the file doesn't imply that menu setup does network I/O.


Generated by Claude Code

@jensenpat
jensenpat marked this pull request as draft July 17, 2026 16:22
@jensenpat
jensenpat marked this pull request as ready for review July 19, 2026 03:23
@ten9876

ten9876 commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

First-pass triage review (not exhaustive — surfacing blockers to keep this moving):

Blockers

  • Music keeps playing after Close. ContributeDialog extends PersistentDialog, whose closeEvent only saves geometry — closing hides the dialog and the instance is retained (m_contributeDialog is a reused QPointer). The QSoundEffect is started with setLoopCount(Infinite) + music->play() in the constructor (Contribute.cpp:684, 693) and nothing pauses/stops it on hide or close. So after the first open, the looping track plays forever in the background even with the dialog dismissed, until app exit. Needs a hideEvent/closeEvent (or visibility-driven) stop/pause of the sound effect. While you're there: the 16 ms m_animationTimer (Contribute.cpp:127-130) also runs continuously on the hidden, retained widget — gate it on visibility so a closed dialog isn't waking 60×/s.

Secondary notes

  • Reopen path: because play() and fetchOpenCollectiveSupporters() only run in the constructor (which showOrRaisePersistent invokes lazily once), a close→reopen won't restart music or re-fetch supporters. Once the timer/music are gated on visibility per the blocker, make sure reopen resumes playback and re-triggers (or reuses) the supporter fetch.
  • DXYCP scroller (paintDxycpScroller) does a per-glyph save/rotate/scale/drawText ×2 every frame; with up to 50 live supporter names joined into m_scrollText this is a few hundred transformed antialiased text ops per frame at 60 fps. Fine while open, but worth a glance on lower-end hardware — consider caching glyph runs or capping scroll-text length.
  • The Open Collective slug (account(slug: "aethersdr"), Contribute.cpp:577) is unverified here — confirm it matches the real collective, else the scroller silently stays on the generic fallback.

Nice, self-contained extraction and the live-fetch-with-fallback design sidesteps stale hardcoded names. The music-after-close lifetime issue is the one thing I'd want fixed before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants