Skip to content

Tone App - #48

Open
NellowTCS wants to merge 3 commits into
TactilityProject:mainfrom
NellowTCS:tone-app
Open

NellowTCS wants to merge 3 commits into
TactilityProject:mainfrom
NellowTCS:tone-app

Conversation

@NellowTCS

@NellowTCS NellowTCS commented Sep 26, 2026 •

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features
    • Added a Tone app with selectable tone and frequency-sweep presets, playback controls, a volume slider, and playback status.
    • The app reports playback issues and stops audio when closed.

@coderabbitai

coderabbitai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

Next included review available in 32 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: fbe8c2e9-5a17-4810-88d1-9a4021df9952

📥 Commits

Reviewing files that changed from the base of the PR and between 5546e2a and ceb8524.

📒 Files selected for processing (4)
  • Apps/Tone/CMakeLists.txt
  • Apps/Tone/main/CMakeLists.txt
  • Apps/Tone/main/Source/App.cpp
  • Apps/Tone/manifest.properties
📝 Walkthrough

Walkthrough

The build matrix adds Tone and TodoList. The change adds Tone’s CMake configuration and manifest. The Tone app provides four frequency presets, including a repeating sweep, and generates 48 kHz, 16-bit stereo audio. Its interface provides playback controls, a volume slider, and playback status. The app reports device, stream, task, and write failures. On close, it stops playback and waits for the playback task to exit.

Priority: ⬇️ Low

Merge Risk: 🟠 High · up to 5546e

Closing the Tone app during playback can make the audio task touch memory that has already been freed and crash the device. Pressing Play again while playback is still stopping can break the new playback session. Fix both before merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 5546e

Playback can overlap during a quick stop and restart, and closing the app may finish before its playback task has stopped using app-owned state. These are locally triggered risks; broader device impact is not established.

Retained concerns

  • Medium · security · inferred: Stop does not await the old playback task. An immediate restart can replace the shared stream handle while that task still runs; its cleanup can then close the new stream, or both tasks can continue against shared state.
  • High · security · inferred: Shutdown uses a null task handle as its completion signal, but the playback task clears that handle before its final Context and UI accesses. Closing the app can therefore destroy state still in use by the task.
  • Medium · reliability · inferred: Closing the app waits for a playback task that may be inside an audio write with an unbounded timeout. If the device does not complete or interrupt that write, the stop flag cannot complete shutdown.
Security review details

Security Blast Radius

  • inferred — The demonstrated trigger is local interaction with Tone's controls or app close, and the immediate affected resources are its task, UI state, and selected audio stream. Host-wide impact or remote reachability is not established.

Security Findings and Attack Paths

  • inferred — Quick stop and restart can make concurrent playback instances share a replaceable stream handle. Closing during playback can also release app-owned state after the task signals completion but before its final accesses. Both paths require local app interaction; no privilege escalation is demonstrated.

Trust Boundaries and Controls

  • observed — The callbacks send bounded local UI selections to audio-device operations. No network, tenant selector, credential use, or authorization change appears in the examined Tone paths.

Resilience and Maintainability Implications

  • inferred — A stop request alone does not interrupt the visible unbounded write. The resulting shutdown risk depends on audio-driver behavior that was not established; the analogous SfxEngine write uses a finite timeout.

Hardening Proposals

  • proposed — Make stream ownership per playback instance, prevent restart until the prior instance finishes, and signal completion only after all Context and UI accesses are done. Establish a bounded or cancellable write path before waiting for shutdown.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 1 files. (4 skipped: 4… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: adding the Tone app. It is concise and directly related to the pull request contents.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 1 files. (4 skipped: 4 unsupported.)

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: a05f571a-0d0d-44e7-aabd-73562038d3b6

📥 Commits

Reviewing files that changed from the base of the PR and between 085c5db and 5546e2a.

📒 Files selected for processing (5)
  • .github/workflows/main.yml
  • Apps/Tone/CMakeLists.txt
  • Apps/Tone/main/CMakeLists.txt
  • Apps/Tone/main/Source/App.cpp
  • Apps/Tone/manifest.properties

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment on lines +186 to +195
audio_stream_close(ctx->streamHandle);
ctx->streamHandle = nullptr;
ctx->playbackTask = nullptr;

lvgl_lock();
updateStatusLabel(ctx);
updatePlayButton(ctx);
lvgl_unlock();

vTaskDelete(nullptr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Clear playbackTask only after the task stops using ctx.

The task sets ctx->playbackTask = nullptr at Line 188. After that, it calls lvgl_lock() and updates widgets through ctx. On close, main stops waiting as soon as playbackTask is null. It then calls window_manager_remove and returns, which destroys the stack-allocated Context ctx. The task can then read freed ctx data and deleted LVGL widgets. This is a use-after-free. playbackTask is also a plain field that two tasks share without synchronization.

Make the handshake an atomic flag. Set it as the last step before vTaskDelete. Skip the UI update when the app is closing.

🐛 Proposed fix
-    TaskHandle_t playbackTask = nullptr;
+    TaskHandle_t playbackTask = nullptr;
+    std::atomic<bool> taskRunning { false };
+    std::atomic<bool> closing { false };
     audio_stream_close(ctx->streamHandle);
     ctx->streamHandle = nullptr;
-    ctx->playbackTask = nullptr;
-
-    lvgl_lock();
-    updateStatusLabel(ctx);
-    updatePlayButton(ctx);
-    lvgl_unlock();
+    if (!ctx->closing.load()) {
+        lvgl_lock();
+        updateStatusLabel(ctx);
+        updatePlayButton(ctx);
+        lvgl_unlock();
+    }
+    ctx->playbackTask = nullptr;
+    ctx->taskRunning.store(false); // last access to ctx
 
     vTaskDelete(nullptr);
-                ctx.playing.store(false);
-                while (ctx.playbackTask != nullptr) {
+                ctx.closing.store(true);
+                ctx.playing.store(false);
+                while (ctx.taskRunning.load()) {
                     vTaskDelay(pdMS_TO_TICKS(10));
                 }

Before calling xTaskCreate, set taskRunning to true. If task creation fails, set it back to false.

Also applies to: 414-419

Comment on lines +201 to +229
if (ctx->playing.load()) {
ctx->playing.store(false);
lv_label_set_text(ctx->statusLabel, "Stopping...");
updatePlayButton(ctx);
return;
}

Device* streamDevice = nullptr;
if (device_get_first_by_type(&AUDIO_STREAM_TYPE, &streamDevice) != ERROR_NONE || streamDevice == nullptr) {
lv_label_set_text(ctx->statusLabel, "No audio stream device");
return;
}
ctx->streamDevice = streamDevice;

const AudioStreamConfig config = {
.sample_rate = SAMPLE_RATE,
.bits_per_sample = BITS_PER_SAMPLE,
.channels = CHANNELS,
};

AudioStreamHandle handle = nullptr;
if (audio_stream_open_output(streamDevice, &config, &handle) != ERROR_NONE) {
lv_label_set_text(ctx->statusLabel, "Failed to open output stream");
return;
}

ctx->streamHandle = handle;
ctx->playing.store(true);
BaseType_t taskResult = xTaskCreate(playbackTask, "tone-playback", PLAYBACK_TASK_STACK_BYTES, ctx, PLAYBACK_TASK_PRIORITY, &ctx->playbackTask);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Block Play while the previous playback task is still stopping.

Pressing Stop only sets playing to false. The old task can still be blocked in audio_stream_write. If the user presses Play during this window, onPlayPressed opens a new stream. It then overwrites ctx->streamHandle and ctx->playbackTask. When the old task exits, it closes ctx->streamHandle at Line 186. That handle is now the new stream, so the new task writes to a closed handle. The new stream from Line 222 is closed early, and the old stream is closed twice or its handle is lost. The old task also clears ctx->playbackTask for the new task. The close wait at Line 416 can then return while the new task is still running.

Return early while a task still exists. Pass the stream handle to the task as its own copy.

🐛 Proposed fix
     if (ctx->playing.load()) {
         ctx->playing.store(false);
         lv_label_set_text(ctx->statusLabel, "Stopping...");
         updatePlayButton(ctx);
         return;
     }
+    if (ctx->playbackTask != nullptr) {
+        return; // previous task is still shutting down
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (ctx->playing.load()) {
ctx->playing.store(false);
lv_label_set_text(ctx->statusLabel, "Stopping...");
updatePlayButton(ctx);
return;
}
Device* streamDevice = nullptr;
if (device_get_first_by_type(&AUDIO_STREAM_TYPE, &streamDevice) != ERROR_NONE || streamDevice == nullptr) {
lv_label_set_text(ctx->statusLabel, "No audio stream device");
return;
}
ctx->streamDevice = streamDevice;
const AudioStreamConfig config = {
.sample_rate = SAMPLE_RATE,
.bits_per_sample = BITS_PER_SAMPLE,
.channels = CHANNELS,
};
AudioStreamHandle handle = nullptr;
if (audio_stream_open_output(streamDevice, &config, &handle) != ERROR_NONE) {
lv_label_set_text(ctx->statusLabel, "Failed to open output stream");
return;
}
ctx->streamHandle = handle;
ctx->playing.store(true);
BaseType_t taskResult = xTaskCreate(playbackTask, "tone-playback", PLAYBACK_TASK_STACK_BYTES, ctx, PLAYBACK_TASK_PRIORITY, &ctx->playbackTask);
if (ctx->playing.load()) {
ctx->playing.store(false);
lv_label_set_text(ctx->statusLabel, "Stopping...");
updatePlayButton(ctx);
return;
}
if (ctx->playbackTask != nullptr) {
return; // previous task is still shutting down
}
Device* streamDevice = nullptr;
if (device_get_first_by_type(&AUDIO_STREAM_TYPE, &streamDevice) != ERROR_NONE || streamDevice == nullptr) {
lv_label_set_text(ctx->statusLabel, "No audio stream device");
return;
}
ctx->streamDevice = streamDevice;
const AudioStreamConfig config = {
.sample_rate = SAMPLE_RATE,
.bits_per_sample = BITS_PER_SAMPLE,
.channels = CHANNELS,
};
AudioStreamHandle handle = nullptr;
if (audio_stream_open_output(streamDevice, &config, &handle) != ERROR_NONE) {
lv_label_set_text(ctx->statusLabel, "Failed to open output stream");
return;
}
ctx->streamHandle = handle;
ctx->playing.store(true);
BaseType_t taskResult = xTaskCreate(playbackTask, "tone-playback", PLAYBACK_TASK_STACK_BYTES, ctx, PLAYBACK_TASK_PRIORITY, &ctx->playbackTask);

@KenVanHoeylandt

Copy link
Copy Markdown
Contributor

Neat!
Can you add a screenshot?
Ideally an extra one on a tiny device too, like a cardputer.

@KenVanHoeylandt

Copy link
Copy Markdown
Contributor

Seems like I broke the build. I'll fix it.

@KenVanHoeylandt

KenVanHoeylandt commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

@NellowTCS I fixed things. Please pull in latest from main.

You'll get a conflict with .github/workflows/main.yml: disregard your changes and accept mine instead.
I made it so that the list of apps is now automatically generated.

edit: I was able to resolve it myself.

Signed-off-by: Ken Van Hoeylandt <git@kenvanhoeylandt.net>
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