Compare commits
2 Commits
6f66a5db64
...
a3e6f6415f
| Author | SHA1 | Date | |
|---|---|---|---|
| a3e6f6415f | |||
| 60c8255c2d |
@@ -41,7 +41,8 @@
|
||||
"Bash(awk '/int REAPERAPI_LoadAPI/,0' /Users/p4piwabl0/Desktop/projects/virtual-controller-clean/extension-reaper-macos/vendor/reaper-sdk/sdk/reaper_plugin_functions.h)",
|
||||
"Bash(grep -n \"{NULL, NULL}\")",
|
||||
"Bash(ls -la \"/Applications/REAPER.app/Contents/MacOS/\" 2>&1 | head -5 *)",
|
||||
"Read(//Applications/REAPER.app/Contents/MacOS/**)"
|
||||
"Read(//Applications/REAPER.app/Contents/MacOS/**)",
|
||||
"Bash(src/.venv/Scripts/python.exe -m PyInstaller --name VirtualController --onedir --windowed --distpath dist --workpath build --specpath . --add-data \"src/app-presets/presets.json;app-presets\" src/main.py)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
venv/
|
||||
.venv/
|
||||
|
||||
__pycache__/
|
||||
|
||||
@@ -6,6 +7,10 @@ __pycache__/
|
||||
|
||||
.DS_Store
|
||||
|
||||
# PyInstaller output (app-desktop)
|
||||
app-desktop/build/
|
||||
app-desktop/dist/
|
||||
|
||||
# Xcode
|
||||
*.xcuserstate
|
||||
xcuserdata/
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['src\\main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('src/app-presets/presets.json', 'app-presets')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='VirtualController',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='VirtualController',
|
||||
)
|
||||
@@ -9,7 +9,26 @@ from PyQt6.QtWidgets import (
|
||||
)
|
||||
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
if getattr(sys, "frozen", False):
|
||||
# Packaged exe: Program Files (or wherever it's installed) is not
|
||||
# writable by a normal user, so presets live in the per-user AppData
|
||||
# folder instead — standard Windows convention for app-written data.
|
||||
PRESETS_DIR = os.path.join(os.environ["APPDATA"], "VirtualController")
|
||||
PRESETS_FILE = os.path.join(PRESETS_DIR, "presets.json")
|
||||
os.makedirs(PRESETS_DIR, exist_ok=True)
|
||||
if not os.path.exists(PRESETS_FILE):
|
||||
# First launch on this machine: seed from the presets.json bundled
|
||||
# into the exe (sys._MEIPASS), so a fresh install starts with the
|
||||
# real presets instead of empty. Only happens once — after this,
|
||||
# AppData is the only copy that's ever read or written.
|
||||
_bundled = os.path.join(sys._MEIPASS, "app-presets", "presets.json")
|
||||
if os.path.exists(_bundled):
|
||||
shutil.copyfile(_bundled, PRESETS_FILE)
|
||||
else:
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PRESETS_DIR = os.path.join(BASE_DIR, "app-presets")
|
||||
PRESETS_FILE = os.path.join(PRESETS_DIR, "presets.json")
|
||||
os.makedirs(PRESETS_DIR, exist_ok=True)
|
||||
@@ -0,0 +1,5 @@
|
||||
import os
|
||||
import runpy
|
||||
|
||||
_here = os.path.dirname(os.path.abspath(__file__))
|
||||
runpy.run_path(os.path.join(_here, "main.py"), run_name="__main__")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,97 @@
|
||||
# Focus Fader Stutter — Bisect Log
|
||||
|
||||
Companion to FEEDBACK_STUTTER_BUG.md. One kill-switch per suspect; flip one,
|
||||
rebuild, drag a Focus Fader while 60hz feedback streams, record the result here,
|
||||
then either keep it off or restore it and move to the next.
|
||||
|
||||
## Ranked suspect list
|
||||
|
||||
| # | Suspect | Where | Switch |
|
||||
|---|---------|-------|--------|
|
||||
| 1 | appendLog per message (even when panel hidden) | ArrangeFunctionsWebSocket.appendLog | `DEBUG_DISABLE_APPEND_LOG` |
|
||||
| 2 | logScrollView.post(fullScroll) per message | inside appendLog | covered by #1 |
|
||||
| 3 | Runnable flood (~3 posts/msg, 300+/sec) | appendLog + dispatch handlers | mostly covered by #1 |
|
||||
| 4 | flashCard per message at 60hz | ArrangeFunctionsWebSocket.flashCard → ArrangeCardView.flashActivity | `DEBUG_DISABLE_NETWORK_FLASH` |
|
||||
| 5 | setText at 60hz on feedback TextView | FocusFeedbackWidgetView.setTextFromNetwork | `DEBUG_DISABLE_FEEDBACK_SETTEXT` |
|
||||
| 6 | LAYER_TYPE_SOFTWARE + blur glow on fader | FocusFaderWidgetView ctor (also FaderWidgetView) | `DEBUG_DISABLE_GLOW` (one per class) |
|
||||
| 7 | new LinearGradient every onDraw | FocusFaderWidgetView.onDraw (also FaderWidgetView) | `DEBUG_DISABLE_GRADIENT_FILL` (one per class) |
|
||||
| 8 | Log.d full-JSON concat per message | ArrangeFunctionsWebSocket.onMessage | `DEBUG_DISABLE_RECEIVE_LOGCAT` |
|
||||
| 9 | Echo stream (sent values come back, run appendLog+flashCard mid-drag) | handleWidgetUpdateF | partially covered by #1 |
|
||||
| 10 | vibrator.vibrate at value edges | FocusFaderWidgetView.checkEdgeHaptic | not yet added |
|
||||
|
||||
## Verdict (2026-07-31)
|
||||
|
||||
**appendLog was the stutter.** A/B confirmed: re-enabling it brings the stutter
|
||||
back immediately. Receive-logcat kept off as a freebie (no felt difference).
|
||||
Everything else exonerated and restored to original behavior — including the
|
||||
glow/software-layer rendering, which Paul tested and cleared.
|
||||
|
||||
Final switch states: `DEBUG_DISABLE_APPEND_LOG = true`,
|
||||
`DEBUG_DISABLE_RECEIVE_LOGCAT = true`, all others `false` (original behavior).
|
||||
|
||||
**Timecode fluidity root cause (found by Paul, same day):** REAPER's audio
|
||||
buffer size. REAPER emits OSC feedback per audio block, so a large buffer
|
||||
makes updates arrive in chunky bursts — uneven cadence no client-side
|
||||
rendering can smooth. Lowering the buffer restored fluid feedback. The
|
||||
drawText rewrite of FocusFeedbackWidgetView (done just before this discovery)
|
||||
stays: it's the cheapest render path and collapses same-frame updates to one
|
||||
draw regardless of source pacing.
|
||||
|
||||
## Test runs
|
||||
|
||||
### Run 1 — DEBUG_DISABLE_APPEND_LOG = true (suspects 1+2, most of 3)
|
||||
- Date: 2026-07-31
|
||||
- Switches off: appendLog (all in/out log lines + auto-scroll posts)
|
||||
- Side effect while off: debug log panel shows nothing new when opened
|
||||
- Result: **CONFIRMED CULPRIT — "way smoother"** with appendLog disabled.
|
||||
- A/B re-test same day: re-enabled appendLog, stutter came back immediately;
|
||||
disabled again. Not a placebo — appendLog is definitively implicated.
|
||||
Left OFF for now; permanent fix TBD.
|
||||
|
||||
### Run 2 — DEBUG_DISABLE_RECEIVE_LOGCAT = true (suspect 8)
|
||||
- Date: 2026-07-31
|
||||
- Switches off: appendLog (kept off from Run 1) + per-message Log.d in onMessage
|
||||
- Side effect while off: incoming messages no longer visible in logcat
|
||||
- Result: **No perceptible difference.** Kept off anyway (free optimization,
|
||||
nobody reads logcat during performance).
|
||||
|
||||
### Run 3 — DEBUG_DISABLE_GLOW = true (suspect 6)
|
||||
- Date: 2026-07-31
|
||||
- Switches off: appendLog + receive-logcat (from Runs 1-2) + shadow-layer glow
|
||||
and software rendering in FocusFaderWidgetView AND FaderWidgetView
|
||||
- Side effect while off: faders lose their glow — plain value line + dot
|
||||
- Result: **Exonerated.** Still smooth with glow off, but no felt difference
|
||||
attributable to it — Paul: "the glow wasn't an issue." Turned back ON
|
||||
(original software-layer glow restored) at end of bisect.
|
||||
|
||||
### Run 4 — DEBUG_DISABLE_NETWORK_FLASH = true (suspect 4)
|
||||
- Date: 2026-07-31
|
||||
- Switches off: appendLog + receive-logcat + glow (Runs 1-3) + network-triggered
|
||||
activity-ring flashes (local touch flashes still work)
|
||||
- Side effect while off: cards don't blink green on desktop-driven updates
|
||||
- Result: **SKIPPED — suspect was already inert.** The tablet's "Activity:
|
||||
ON/OFF" button (default OFF) was off during all stutter tests, and
|
||||
flashActivity() early-returns on that flag before any timer work. Kill
|
||||
switch left in place (harmless, skips a registry lookup) but this suspect
|
||||
is exonerated for the observed stutter.
|
||||
|
||||
### Run 5 — DEBUG_DISABLE_FEEDBACK_SETTEXT = true (suspect 5, plus its share of 3)
|
||||
- Date: 2026-07-31
|
||||
- Switches off: appendLog + receive-logcat + glow (Runs 1-3) + the ENTIRE
|
||||
widget_feedback UI path: the runOnUiThread post, registry lookup, and
|
||||
setText all skipped (placed before the post, so this also removes 60/sec
|
||||
of the runnable flood)
|
||||
- Side effect while off: feedback readouts (timecode etc.) freeze at last text
|
||||
- Result: **Nothing earth-shaking — no clear felt difference.** Switch turned
|
||||
back OFF (readouts are needed regardless). 60hz setText is tolerable now
|
||||
that appendLog is gone; candidate for once-per-frame conflation later, not
|
||||
a primary culprit.
|
||||
|
||||
### Run 6 — DEBUG_DISABLE_GRADIENT_FILL = true (suspect 7)
|
||||
- Date: 2026-07-31
|
||||
- Switches off: appendLog + receive-logcat + glow (kept from earlier runs);
|
||||
gradient fill replaced with flat translucent fill in both fader classes
|
||||
- Side effect while off: fader fill is flat color, no fade-to-transparent
|
||||
- Result: **No felt difference — gradient turned back ON** (look kept).
|
||||
Create-once gradient (setLocalMatrix) stays on the permanent-fix list as
|
||||
hygiene, not a culprit.
|
||||
@@ -1,5 +1,13 @@
|
||||
# Android Feedback Widget Stutter Bug
|
||||
|
||||
> **RESOLVED 2026-07-31** — two separate root causes: (1) fader stutter was
|
||||
> `appendLog()` running per-message on the main thread even with the log panel
|
||||
> hidden (A/B confirmed, disabled via kill switch); (2) chunky timecode was
|
||||
> REAPER's audio buffer size making OSC feedback leave in bursts. Full
|
||||
> write-up: `PERF_SESSION_2026-07-31.md`; run-by-run record: `BISECT_LOG.md`.
|
||||
> The "custom Canvas view" hypothesis below was also acted on —
|
||||
> FocusFeedbackWidgetView now draws via canvas.drawText.
|
||||
|
||||
## Summary
|
||||
When the Android tablet app receives high-frequency network feedback updates (60hz timecode from desktop), the feedback widget AND the fader control stutter and become unresponsive. Works perfectly on iOS and desktop apps at the same 60hz update rate.
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Performance Session — 2026-07-31
|
||||
|
||||
Android tablet remote: fader stutter + timecode feedback fluidity. Both root
|
||||
causes found and fixed. Companion docs: `FEEDBACK_STUTTER_BUG.md` (original
|
||||
symptom report, now resolved), `BISECT_LOG.md` (run-by-run test record).
|
||||
|
||||
## The two problems (they were separate)
|
||||
|
||||
### 1. Fader stutter under 60hz feedback → appendLog
|
||||
|
||||
**Root cause:** `appendLog()` in `ArrangeFunctionsWebSocket.java` ran on every
|
||||
in/out WebSocket message (~110/sec during a drag with feedback streaming),
|
||||
even though the log panel is hidden by default:
|
||||
|
||||
- `TextView.append()` on a selectable (editable/spannable) TextView — cost
|
||||
grows as the text grows
|
||||
- 2-3 posted main-thread runnables per message (~300/sec), queued ahead of
|
||||
touch events
|
||||
- Every ~0.5s, the line-trim: full 350-line copy/split/join/setText — a
|
||||
multi-millisecond spike that blew the 16ms frame budget. Periodic spikes =
|
||||
the rhythmic stutter.
|
||||
|
||||
**Proof:** A/B tested — kill switch off = smooth, on = stutter returns
|
||||
immediately. Second time this exact code was convicted (an earlier session
|
||||
already replaced get+concat+setText with append; the remaining per-message
|
||||
work was still the culprit).
|
||||
|
||||
**Fix (current):** `DEBUG_DISABLE_APPEND_LOG = true` — appendLog fully off.
|
||||
Side effect: log panel shows nothing. Shippable fix still TODO (see below).
|
||||
|
||||
### 2. Chunky timecode readout → REAPER buffer size
|
||||
|
||||
**Root cause (found by Paul):** REAPER emits OSC feedback per audio block. A
|
||||
high audio buffer size makes feedback updates leave REAPER in uneven bursts —
|
||||
no client-side rendering can smooth a chunky source. Lowering the buffer
|
||||
restored fluid timecode.
|
||||
|
||||
**Rule of thumb:** if timecode/dB readouts ever look chunky again on any
|
||||
client (Android, iOS, desktop log), check REAPER's audio buffer size FIRST,
|
||||
before suspecting client code.
|
||||
|
||||
## Changes made this session
|
||||
|
||||
| Change | File | Status |
|
||||
|---|---|---|
|
||||
| Kill switch: appendLog off | `arrange/ArrangeFunctionsWebSocket.java` | **Active** (`DEBUG_DISABLE_APPEND_LOG = true`) |
|
||||
| Kill switch: per-message `Log.d` off | same | **Active** (`DEBUG_DISABLE_RECEIVE_LOGCAT = true`) — no felt difference, kept as freebie |
|
||||
| Kill switch: network activity-flash | same | Inactive (`false`) — exonerated; tablet's Activity button (default OFF) already gated it |
|
||||
| Kill switch: feedback setText | same | Inactive (`false`) — exonerated, 60hz setText was affordable |
|
||||
| Kill switch: fader glow / software layer | `widgets/FaderWidgetView.java`, `widgets/FocusFaderWidgetView.java` | Inactive (`false`) — exonerated by Paul, glow restored |
|
||||
| Kill switch: gradient fill alloc | same two files | Inactive (`false`) — exonerated, gradient restored |
|
||||
| **FocusFeedbackWidgetView rewritten: TextView → bare View + `canvas.drawText`** | `widgets/FocusFeedbackWidgetView.java` | **Permanent.** Update = string compare + field assign + invalidate; no text Layout object per update; same-frame updates collapse to one draw. All sizing/pinch/font-weight/color behavior preserved. |
|
||||
| Color picker: explicit branch for new view type | `arrange/ContextMenuViewLogic.java` | Permanent — was matching via `instanceof TextView`, which the rewrite would have silently broken |
|
||||
|
||||
## The lesson (codebase rule)
|
||||
|
||||
Main thread = one queue for touch, draw, and posted runnables. Per-message
|
||||
work is fine if **small and constant** (setText, a flash, a field assign).
|
||||
Banned on the message stream: anything **unbounded** (grows with history) or
|
||||
**spiky** (periodic big jobs). appendLog was both, for an invisible panel.
|
||||
|
||||
## Follow-ups (not yet done)
|
||||
|
||||
1. **Shippable appendLog fix** — replace the kill switch: log only while the
|
||||
panel is visible; bounded ring buffer for history so opening the panel
|
||||
isn't empty; then delete `DEBUG_DISABLE_APPEND_LOG`.
|
||||
2. Decide fate of the exonerated kill switches (probably delete for cleanliness;
|
||||
they're documented in BISECT_LOG.md if ever needed again).
|
||||
3. Optional hygiene: create the fader fill gradient once (setLocalMatrix)
|
||||
instead of per-onDraw; gate `Log.d` behind `BuildConfig.DEBUG` instead of a
|
||||
custom flag.
|
||||
4. Consider documenting the REAPER-buffer/feedback-cadence relationship in
|
||||
CLAUDE.md's DAW integration section.
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
-1
@@ -1,4 +1,4 @@
|
||||
#Thu Jul 30 19:08:15 CDT 2026
|
||||
#Fri Jul 31 17:37:31 CDT 2026
|
||||
base.0=E\:\\20260608-virtual-controller-py\\remote-client-android\\app\\build\\intermediates\\dex\\debug\\mergeExtDexDebug\\classes.dex
|
||||
base.1=E\:\\20260608-virtual-controller-py\\remote-client-android\\app\\build\\intermediates\\dex\\debug\\mergeProjectDexDebug\\0\\classes.dex
|
||||
base.2=E\:\\20260608-virtual-controller-py\\remote-client-android\\app\\build\\intermediates\\dex\\debug\\mergeProjectDexDebug\\13\\classes.dex
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+22
-1
@@ -37,6 +37,24 @@ import okio.ByteString;
|
||||
public class ArrangeFunctionsWebSocket {
|
||||
private static final String TAG = "VCWebSocket";
|
||||
|
||||
// ---- Stutter-bisect kill switches (see BISECT_LOG.md) ----
|
||||
// Flip to false to restore the original behavior. Suspect #1+#2 in the
|
||||
// Focus Fader lag investigation: appendLog runs on EVERY in/out message
|
||||
// (~110/sec under 60hz feedback) even while the log panel is hidden.
|
||||
private static final boolean DEBUG_DISABLE_APPEND_LOG = true;
|
||||
// Suspect #8: Log.d with full-JSON string concat on EVERY incoming message
|
||||
// (~110/sec under 60hz feedback) — runs on the OkHttp reader thread, but
|
||||
// the concat allocs + logd writes are pure per-message overhead.
|
||||
private static final boolean DEBUG_DISABLE_RECEIVE_LOGCAT = true;
|
||||
// Suspect #4: activity-ring flash on every incoming network message —
|
||||
// removeCallbacks + postDelayed on the main thread at 60hz+ per widget.
|
||||
// Local touches still flash their card; only network-triggered flashes stop.
|
||||
private static final boolean DEBUG_DISABLE_NETWORK_FLASH = false;
|
||||
// Suspect #5: setText() on the feedback TextView at 60hz — new text layout
|
||||
// + redraw on the main thread per message. While true, feedback readouts
|
||||
// (timecode etc.) freeze at their last shown text.
|
||||
private static final boolean DEBUG_DISABLE_FEEDBACK_SETTEXT = false;
|
||||
|
||||
private final ArrangeActivity vc;
|
||||
private final OkHttpClient client = new OkHttpClient();
|
||||
|
||||
@@ -65,7 +83,7 @@ public class ArrangeFunctionsWebSocket {
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, String text) {
|
||||
Log.d(TAG, "Received: " + text);
|
||||
if (!DEBUG_DISABLE_RECEIVE_LOGCAT) Log.d(TAG, "Received: " + text);
|
||||
dispatch(text);
|
||||
}
|
||||
|
||||
@@ -291,6 +309,7 @@ public class ArrangeFunctionsWebSocket {
|
||||
String uid = envelope.optString("uid", "");
|
||||
String text = envelope.optString("text", "");
|
||||
appendLog("IN", "widget_feedback uid=" + uid + " text=\"" + text + "\"");
|
||||
if (DEBUG_DISABLE_FEEDBACK_SETTEXT) return;
|
||||
vc.runOnUiThread(() -> {
|
||||
TextUpdatable widget = vc.feedbackRegistry.get(uid);
|
||||
if (widget != null) widget.setTextFromNetwork(text);
|
||||
@@ -300,6 +319,7 @@ public class ArrangeFunctionsWebSocket {
|
||||
|
||||
/** Activity-ring flash on the card wrapping this uid — matches iOS's BaseWidget.update(value:) side effect. */
|
||||
private void flashCard(String uid) {
|
||||
if (DEBUG_DISABLE_NETWORK_FLASH) return;
|
||||
ArrangeCardView card = vc.cardRegistry.get(uid);
|
||||
if (card != null) card.flashActivity();
|
||||
}
|
||||
@@ -330,6 +350,7 @@ public class ArrangeFunctionsWebSocket {
|
||||
* run on every call, just often enough to bound memory growth.
|
||||
*/
|
||||
private void appendLog(String direction, String message) {
|
||||
if (DEBUG_DISABLE_APPEND_LOG) return;
|
||||
String line = "[" + direction + "] " + message + "\n";
|
||||
vc.runOnUiThread(() -> {
|
||||
vc.outputTextView.append(line);
|
||||
|
||||
+5
-1
@@ -81,7 +81,11 @@ public class ContextMenuViewLogic {
|
||||
@Override
|
||||
public void onColorSelected(int colorIndex) {
|
||||
int colorValue = ContextMenuView.COLORS[colorIndex];
|
||||
if (controlView instanceof android.widget.TextView) {
|
||||
// FocusFeedbackWidgetView is a bare canvas View (drawText), not a
|
||||
// TextView — it needs its own branch or the instanceof misses it.
|
||||
if (controlView instanceof FocusFeedbackWidgetView) {
|
||||
((FocusFeedbackWidgetView) controlView).setTextColor(colorValue);
|
||||
} else if (controlView instanceof android.widget.TextView) {
|
||||
((android.widget.TextView) controlView).setTextColor(colorValue);
|
||||
}
|
||||
String colorHex = String.format("#%06X", (0xFFFFFF & colorValue));
|
||||
|
||||
+19
-2
@@ -33,6 +33,16 @@ public class FaderWidgetView extends FrameLayout implements UpdatableWidget {
|
||||
private static final int PAD_HEIGHT_DP = 90;
|
||||
private static final float FINE_SENSITIVITY = 0.10f;
|
||||
|
||||
// Stutter-bisect suspect #6 (see BISECT_LOG.md): setShadowLayer glow forces
|
||||
// LAYER_TYPE_SOFTWARE — full CPU re-raster of the fader every drag frame.
|
||||
// true = no glow, hardware rendering; false = original glow look.
|
||||
private static final boolean DEBUG_DISABLE_GLOW = false;
|
||||
|
||||
// Stutter-bisect suspect #7 (see BISECT_LOG.md): a new LinearGradient is
|
||||
// allocated on every onDraw — GC churn during drags. true = flat translucent
|
||||
// fill, zero allocations; false = original gradient fill.
|
||||
private static final boolean DEBUG_DISABLE_GRADIENT_FILL = false;
|
||||
|
||||
private final String uid;
|
||||
private final Paint bgPaint = new Paint();
|
||||
private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
@@ -69,14 +79,16 @@ public class FaderWidgetView extends FrameLayout implements UpdatableWidget {
|
||||
|
||||
glowLinePaint.setColor(colorArgb);
|
||||
glowLinePaint.setStrokeWidth(6f);
|
||||
glowLinePaint.setShadowLayer(18f, 0f, 0f, colorArgb);
|
||||
|
||||
dotPaint.setColor(Color.WHITE);
|
||||
dotPaint.setShadowLayer(10f, 0f, 0f, colorArgb);
|
||||
|
||||
if (!DEBUG_DISABLE_GLOW) {
|
||||
glowLinePaint.setShadowLayer(18f, 0f, 0f, colorArgb);
|
||||
dotPaint.setShadowLayer(10f, 0f, 0f, colorArgb);
|
||||
// Shadow layers only render on a software-rendered layer.
|
||||
setLayerType(LAYER_TYPE_SOFTWARE, null);
|
||||
}
|
||||
}
|
||||
|
||||
public String getUid() {
|
||||
return uid;
|
||||
@@ -131,11 +143,16 @@ public class FaderWidgetView extends FrameLayout implements UpdatableWidget {
|
||||
|
||||
// Transparent at the bottom of the fill, full color+alpha at the value
|
||||
// line — analog of iOS's CAGradientLayer fill.
|
||||
if (DEBUG_DISABLE_GRADIENT_FILL) {
|
||||
fillPaint.setShader(null);
|
||||
fillPaint.setColor(Color.argb(60, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)));
|
||||
} else {
|
||||
fillPaint.setShader(new LinearGradient(
|
||||
0, h, 0, fillTop,
|
||||
Color.argb(0, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)),
|
||||
Color.argb(115, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)),
|
||||
Shader.TileMode.CLAMP));
|
||||
}
|
||||
canvas.drawRect(0, fillTop, w, h, fillPaint);
|
||||
|
||||
// Glowing value line + a small glowing dot at its center — analog of the
|
||||
|
||||
+19
-1
@@ -36,6 +36,16 @@ public class FocusFaderWidgetView extends FrameLayout implements UpdatableWidget
|
||||
private static final float FINE_SENSITIVITY = 0.10f;
|
||||
private static final int PAD_HEIGHT_DP = 90;
|
||||
|
||||
// Stutter-bisect suspect #6 (see BISECT_LOG.md): setShadowLayer glow forces
|
||||
// LAYER_TYPE_SOFTWARE — full CPU re-raster of the fader every drag frame.
|
||||
// true = no glow, hardware rendering; false = original glow look.
|
||||
private static final boolean DEBUG_DISABLE_GLOW = false;
|
||||
|
||||
// Stutter-bisect suspect #7 (see BISECT_LOG.md): a new LinearGradient is
|
||||
// allocated on every onDraw — GC churn during drags. true = flat translucent
|
||||
// fill, zero allocations; false = original gradient fill.
|
||||
private static final boolean DEBUG_DISABLE_GRADIENT_FILL = false;
|
||||
|
||||
private final String uid;
|
||||
private final Paint bgPaint = new Paint();
|
||||
private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
@@ -70,11 +80,14 @@ public class FocusFaderWidgetView extends FrameLayout implements UpdatableWidget
|
||||
borderPaint.setStrokeWidth(4f);
|
||||
glowLinePaint.setColor(colorArgb);
|
||||
glowLinePaint.setStrokeWidth(6f);
|
||||
glowLinePaint.setShadowLayer(18f, 0f, 0f, colorArgb);
|
||||
dotPaint.setColor(Color.WHITE);
|
||||
if (!DEBUG_DISABLE_GLOW) {
|
||||
glowLinePaint.setShadowLayer(18f, 0f, 0f, colorArgb);
|
||||
dotPaint.setShadowLayer(10f, 0f, 0f, colorArgb);
|
||||
// Shadow layers only render on a software-rendered layer.
|
||||
setLayerType(LAYER_TYPE_SOFTWARE, null);
|
||||
}
|
||||
}
|
||||
|
||||
public String getUid() {
|
||||
return uid;
|
||||
@@ -118,11 +131,16 @@ public class FocusFaderWidgetView extends FrameLayout implements UpdatableWidget
|
||||
float fillHeight = Math.max(8f, h * value);
|
||||
float fillTop = h - fillHeight;
|
||||
|
||||
if (DEBUG_DISABLE_GRADIENT_FILL) {
|
||||
fillPaint.setShader(null);
|
||||
fillPaint.setColor(Color.argb(60, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)));
|
||||
} else {
|
||||
fillPaint.setShader(new LinearGradient(
|
||||
0, h, 0, fillTop,
|
||||
Color.argb(0, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)),
|
||||
Color.argb(115, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)),
|
||||
Shader.TileMode.CLAMP));
|
||||
}
|
||||
canvas.drawRect(0, fillTop, w, h, fillPaint);
|
||||
|
||||
canvas.drawLine(0, fillTop, w, fillTop, glowLinePaint);
|
||||
|
||||
+43
-21
@@ -1,14 +1,14 @@
|
||||
package com.virtualcontroller.remote.widgets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.text.TextPaint;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.widget.TextView;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* Read-only Channel Focus DAW-state readout — updated live via
|
||||
@@ -21,8 +21,16 @@ import android.widget.TextView;
|
||||
* drives the font size and lets the frame follow, rather than scaling the
|
||||
* frame like every other widget type — the aspect ratio then can't drift out
|
||||
* of sync with the text it's wrapping.
|
||||
*
|
||||
* Draws via canvas.drawText on a bare View rather than extending TextView:
|
||||
* this readout repaints at up to 60hz (timecode), and TextView.setText builds
|
||||
* a new internal text Layout on every call — measurement, line-breaking, span
|
||||
* bookkeeping — all for one line of digits that never wraps. drawText skips
|
||||
* every bit of that: a network update is now just a String field assign +
|
||||
* invalidate, and the draw is a single glyph run. (Same conclusion
|
||||
* FEEDBACK_STUTTER_BUG.md guessed TouchOSC reached.)
|
||||
*/
|
||||
public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
public class FocusFeedbackWidgetView extends View implements TextUpdatable {
|
||||
|
||||
public static final float DEFAULT_FONT_SP = 32f;
|
||||
public static final float MIN_FONT_SP = 12f;
|
||||
@@ -36,6 +44,8 @@ public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
void onFitRequest();
|
||||
}
|
||||
|
||||
private final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
||||
private String text;
|
||||
private float fontSp = DEFAULT_FONT_SP;
|
||||
private OnFitRequestListener fitListener;
|
||||
|
||||
@@ -51,14 +61,9 @@ public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
*/
|
||||
public FocusFeedbackWidgetView(Context context, String initialText, int savedHeightPx) {
|
||||
super(context);
|
||||
setText(initialText);
|
||||
setTextColor(TEXT_COLOR);
|
||||
setBackgroundColor(Color.TRANSPARENT);
|
||||
setGravity(Gravity.CENTER);
|
||||
setSingleLine(true);
|
||||
|
||||
int pad = dp(context, PADDING_DP);
|
||||
setPadding(pad, pad, pad, pad);
|
||||
this.text = initialText != null ? initialText : "";
|
||||
paint.setColor(TEXT_COLOR);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
|
||||
int defaultHeight = fittedSize(context, initialText, DEFAULT_FONT_SP)[1];
|
||||
if (savedHeightPx > 0 && defaultHeight > 0) {
|
||||
@@ -72,7 +77,8 @@ public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
}
|
||||
|
||||
public void setTextColor(int color) {
|
||||
super.setTextColor(color);
|
||||
paint.setColor(color);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setFontWeight(String weight) {
|
||||
@@ -83,14 +89,14 @@ public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
tf = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD);
|
||||
} else if ("super_bold".equalsIgnoreCase(weight)) {
|
||||
tf = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD);
|
||||
getPaint().setStrokeWidth(2.0f);
|
||||
paint.setStrokeWidth(2.0f);
|
||||
} else if ("thin".equalsIgnoreCase(weight)) {
|
||||
tf = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL);
|
||||
getPaint().setStrokeWidth(0.5f);
|
||||
paint.setStrokeWidth(0.5f);
|
||||
} else {
|
||||
tf = semiboldMono();
|
||||
}
|
||||
setTypeface(tf);
|
||||
paint.setTypeface(tf);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@@ -109,12 +115,12 @@ public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
|
||||
/** Box size for the current text at the current font size. */
|
||||
public int[] fittedSizePx() {
|
||||
return fittedSize(getContext(), getText().toString(), fontSp);
|
||||
return fittedSize(getContext(), text, fontSp);
|
||||
}
|
||||
|
||||
/** Box size for the current text at DEFAULT_FONT_SP — what "Reset size" converges to. */
|
||||
public int[] defaultFittedSizePx() {
|
||||
return fittedSize(getContext(), getText().toString(), DEFAULT_FONT_SP);
|
||||
return fittedSize(getContext(), text, DEFAULT_FONT_SP);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,15 +148,31 @@ public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
|
||||
@Override
|
||||
public void setTextFromNetwork(String text) {
|
||||
if (text.equals(getText().toString())) return;
|
||||
setText(text);
|
||||
if (text == null || text.equals(this.text)) return;
|
||||
this.text = text;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
// MARK: - Drawing
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
if (text.isEmpty()) return;
|
||||
// Centered both ways: CENTER-aligned paint handles x; y places the
|
||||
// baseline so the glyph block's vertical middle sits at the view's.
|
||||
Paint.FontMetrics fm = paint.getFontMetrics();
|
||||
float y = getHeight() / 2f - (fm.ascent + fm.descent) / 2f;
|
||||
canvas.drawText(text, getWidth() / 2f, y, paint);
|
||||
}
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
private void applyFont() {
|
||||
setTypeface(semiboldMono());
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSp);
|
||||
paint.setTypeface(semiboldMono());
|
||||
paint.setTextSize(TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_SP, fontSp, getResources().getDisplayMetrics()));
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private void requestFit() {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
_root = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.join(_root, "app-desktop-macos"))
|
||||
sys.path.insert(0, os.path.join(_root, "remote-server"))
|
||||
|
||||
import runpy
|
||||
runpy.run_path(os.path.join(_root, "app-desktop-macos", "main.py"), run_name="__main__")
|
||||
Reference in New Issue
Block a user