Commit Graph
105 Commits
Author SHA1 Message Date
Byron GamatosandGitHub 745e360c89 Merge pull request #106 from got-feedBack/feat/bundle-bongocat-plugin
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
feat(build): bundle bongocat plugin
2026-07-13 22:24:26 +02:00
OmikronApexandClaude Fable 5 fcce4672d9 feat(build): bundle bongocat plugin
Add got-feedback/feedback-plugin-bongocat to the clone_slopsmith plugin
list so CI/release builds ship Bongo Cat's Rhythm Trainer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:01:52 +02:00
Byron GamatosandGitHub ffd56922f7 Merge pull request #105 from got-feedBack/feat/start-fullscreen
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
Nightly / setup (push) Has been cancelled
Nightly / build (arm64, macos-14, mac) (push) Has been cancelled
Nightly / build (x64, ubuntu-22.04, linux) (push) Has been cancelled
Nightly / build (x64, windows-latest, win) (push) Has been cancelled
Nightly / publish (push) Has been cancelled
feat: add "start in fullscreen" window preference
2026-07-13 14:44:48 +02:00
byrongamatos 45a05b80a0 Fix fullscreen launch with maximized restore 2026-07-13 14:26:21 +02:00
gionnibgud 4f1b64ff73 feat: add "start in fullscreen" window preference
Adds an opt-in preference that launches the main window in fullscreen,
driven by feedBack core's Settings → System "Fullscreen" toggle. Wires
the window.feedBackDesktop.window bridge (getStartFullscreen /
setStartFullscreen) that core's setupWindowOptions() gates on, persists
the flag in the desktop config (DesktopConfig.startFullscreen, alongside
windowBounds), and passes `fullscreen: true` at BrowserWindow creation
when set.

Persistence lives here (not renderer localStorage) because the main
process must read the pref at window-creation time. setStartFullscreen
live-applies via setFullScreen so the toggle is responsive on
Windows/Linux; on macOS the first programmatic fullscreen-enter on a
window created windowed is dropped by AppKit, so there it takes effect on
next launch — the core Settings copy notes this. This intentionally
narrows the earlier "never launch fullscreen" default to an opt-in.

Signed-off-by: gionnibgud <gionnibgud@gmail.com>
2026-07-13 13:44:33 +02:00
K. O. A.andGitHub 6884800fd8 Merge pull request #104 from got-feedBack/feat/pane-windows-above-main
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
fix(panes): a pane can never go behind the app, and can never be minimized into oblivion
2026-07-12 22:43:17 -04:00
topkoa 47fe1492a6 fix(panes): a minimized pane window was unrecoverable — remove the button
Reported in testing: pop out the stem mixer, minimize it, and it is gone. Not
hidden — gone. It appears in no taskbar, no alt-tab list, nothing.

The chain:

  - A pane window is skipTaskbar, so it never masquerades as a second fee[dB]ack.
  - It is parented to the main window (so it can't go behind the app), which makes
    it an OWNED window — and Windows will not give an owned window a taskbar
    button in any case.
  - On minimize we then hid it "to the tray".

So a minimized pane had no taskbar entry and no alt-tab entry, and the only route
back was the tray — which Windows tucks behind the overflow chevron by default.
"It's in the tray" is not an answer when the user cannot see the tray.

The window is no longer minimizable. Every remaining way to put a pane away is one
the user can undo from something visible:

  - close it        → the panel returns to the app, where it came from
  - the tray        → per-pane toggle, plus Show/Hide all panes
  - the chip's stub → in the app, exactly where the panel used to be

Plus a restore() on 'minimize' in case anything else (a window manager, a
shortcut, a future code path) minimizes it anyway.

This is the same class of mistake as hiding the panel behind the pop-out chip: a
place to put something, with no way back that the user can find.

Signed-off-by: topkoa <topkoa@gmail.com>
2026-07-12 22:36:53 -04:00
topkoa 0878ae2682 feat(panes): a pane window can never go behind the app
A pane is a control surface for the thing you are looking at. Clicking the
highway to play — the single most common thing anyone does here — raises the
main window, and a plain sibling window slides straight behind it. You would
fish the mixer back out from behind the game every time you touched the game.
That is not a pop-out, it is a hiding place.

Parent each pane window to the main window. That is the precise amount of "in
front": the pane always floats above fee[dB]ack, and behaves like any other
window against everything else.

Deliberately NOT setAlwaysOnTop. That would put a pane above the user's browser
and editor too — a surprising thing to inflict on someone for opening a mixer.
alwaysOnTop still composes on top of this for a pane the user explicitly wants
above everything.

The trade, accepted: the OS ties parent and child together, so minimizing
fee[dB]ack hides its panes and restoring brings them back. That is what a
companion window should do.

Failure is non-fatal — a pane that can be buried is still a working pane.

Signed-off-by: topkoa <topkoa@gmail.com>
2026-07-12 22:32:30 -04:00
K. O. A.andGitHub 1133205e87 Merge pull request #103 from got-feedBack/feat/pane-host-tray
feat(panes): pane pop-out windows + the system tray
2026-07-12 21:35:19 -04:00
topkoa 0cde745f03 fix(panes): let toggleWindow be the authority, not a stale hasWindow() check
The tray menu asked "do we own a window for this pane?" and then acted on the
answer. Windows are destroyed asynchronously, so between the question and the
act the answer can go stale: hasWindow() says yes, the window is destroyed,
toggleWindow() returns false, the handler has already committed to the
main-process path and returns — and the click lands on nothing.

A tray item that silently does nothing is the worst possible failure here,
because the tray IS the recovery path when a pane is out of sight.

toggleWindow() already reports whether it did anything. Use that: if it toggled,
we're done; if it didn't, we never had that window (or just lost it), and only
the renderer can decide what opening the pane means — it might belong in the
dock, and its element lives there.

hasWindow()/hasPaneWindow() existed only to support the racy check, so they're
gone rather than left lying around for someone to reintroduce the race with.

Signed-off-by: topkoa <topkoa@gmail.com>
2026-07-12 21:30:38 -04:00
topkoa 43c2e41c07 fix(panes): a corrupt config entry must not take down the main process
savedFor() assumed paneWindows[paneId] was an object. It isn't necessarily:
the desktop config is a JSON file on disk, hand-editable, and writable by any
build. `{"camera_director": null}` passes the own-property check and then throws
on `saved.bounds`.

This runs in the MAIN process, at window-adoption time. A TypeError there is not
a bad pane — it is the app failing.

Anything that isn't a plain object now degrades to "nothing saved", which is
exactly what an unreadable entry means. Same guard on `paneWindows` itself (a
string or an array would have got past `?? {}`).

persist() gets the same treatment, and for a sharper reason: it read the map,
copied every entry forward, and wrote it back. A corrupt entry would have been
faithfully preserved on every save — so a single hand-edit would keep crashing
the next launch, forever. Corrupt entries are now dropped rather than carried.

Signed-off-by: topkoa <topkoa@gmail.com>
2026-07-12 21:24:04 -04:00
topkoa 030559dd38 fix(panes): only the main window may drive the tray; clamp fallback defaults
1. pane:sync was accepted from ANY renderer with the preload bridge. Pane
   windows are same-origin top-level frames, so preload's isMainFrame gate hands
   them the bridge too — which means a pane window (or any allowed pop-up) could
   send pane:sync and overwrite the tray's registry, most simply by pushing an
   empty list and emptying the menu.

   Exactly one renderer owns the pane registry. It is now accepted from that one
   only: event.sender must be the main window's webContents.

2. sanitizeWindowBounds returned sizing.defaultWidth/Height unclamped. The min
   clamp only runs when `saved` parses, so a caller whose defaults undercut its
   own minimums would get a window below the floor on precisely the paths where
   nothing is saved — first launch, or a corrupt config — and a correctly sized
   one everywhere else.

   That is the worst shape a bug can have: invisible in the common case, and
   visible only to a new user. The fallback is clamped to the floor now, with a
   test.

window-bounds: 14/14.
Signed-off-by: topkoa <topkoa@gmail.com>
2026-07-12 21:18:05 -04:00
topkoa 313d1e5d5b fix(panes): persist alwaysOnTop, flush geometry on quit, test the sizing override
Five more from review.

1. alwaysOnTop was RESTORED but never SAVED. A dead read: the only way to turn
   it on was to hand-edit the config file. There is now one snapshot() that
   decides what "remembered" means, used by the debounced save, the flush on
   close, and the flush on quit — so the three can never disagree about it again.

2. GEOMETRY WAS LOST ON QUIT. closeAllPanes() calls destroy(), and destroy()
   does not fire 'close' — so the flush wired to that event never ran on the one
   path every user takes. Combined with the 400ms debounce: move a pane, quit two
   seconds later, and its position was gone. Every pane is flushed before its
   window is destroyed.

3. The tray-icon comment claimed a 16px PNG; build:ts copies the 32px one. Also
   spelled out what the macOS consequence actually is (a colour icon rather than
   one that adapts to light/dark menu bars), rather than gesturing at it.

4. sanitizeWindowBounds' new `sizing` parameter had no tests — and it is the
   whole reason the function was touched. Without it a 380x560 pane restored
   through the MAIN window's 800x600 floor is silently inflated to three times
   the size the plugin asked for. Four tests now pin it: a small pane is not
   inflated, the min clamp uses the override, corrupt input falls back to the
   override's defaults, and — the one that protects everyone else — omitting
   `sizing` leaves the main window's behaviour byte-for-byte unchanged.

window-bounds tests: 13/13.

Signed-off-by: topkoa <topkoa@gmail.com>
2026-07-12 21:12:04 -04:00
topkoa 3d9e8481ea fix(panes): break the tray/host import cycle; debounce saves; restore before show
Five findings from CodeRabbit on #103.

1. CIRCULAR IMPORT. pane-tray imported pane-hosts while pane-hosts imported
   pane-tray. In the main process that is not a style question: whichever module
   loses the load race sees the other's exports half-initialised, and it fails at
   whatever moment the graph happens to resolve in — which is to say, not on your
   machine.

   One direction only now: pane-hosts → pane-tray. What the tray needs from the
   host (toggle/showAll/hideAll/hasWindow/getMainWindow) is INJECTED through
   initTray(), wired in main.ts.

2. SYNCHRONOUS DISK WRITES ON EVERY DRAG FRAME. `save()` was wired straight to
   'moved'/'resized', and setDesktopConfig is writeFileSync + renameSync. On
   macOS that is dozens of blocking writes per second, in the main process,
   while the user drags.

   Debounced to 400ms — and then flushed on 'close', because a debounce that
   drops the last move is worse than no debounce: nudge a pane, close it a moment
   later, and you would lose the position you just chose, which is the exact thing
   remembered geometry exists to prevent. 'close' (not 'closed') because the
   window has to still exist to be measured.

3. A MINIMIZED PANE COULD NOT BE BROUGHT BACK. Panes go to the tray by being
   minimized and then hidden — and hiding a minimized window does not un-minimize
   it. So show() from the tray restored a window that was still minimized:
   present, but not on screen. Which reads as the tray being broken. Everything
   now goes through reveal(), which restores first.

4. PROTOTYPE POLLUTION VIA PANE ID. A pane id arrives from the RENDERER (it is
   the tail of the frame name window.open() supplied) and is used as a KEY in the
   persisted paneWindows map. `__proto__` is not an id, it is a way to mutate
   Object.prototype from a plugin. Rejected on write, the map is rebuilt on a
   null-prototype object, and reads are own-property checks — otherwise a polluted
   or hand-edited config hands back geometry for a pane that was never saved.

5. Removed getMainWindowRef(), exported and referenced nowhere.

Not applicable: the finding about req.width/req.height/req.title reaching
BrowserWindow unvalidated. That was the `pane:open` IPC, which no longer exists —
main does not create pane windows at all now (the renderer must, so it can adopt
its element into them). The sizes now come from the window Electron already made.

Signed-off-by: topkoa <topkoa@gmail.com>
2026-07-12 21:05:11 -04:00
topkoa 938ddded60 feat(panes)!: dress the renderer's pane window, don't create it
Follows the core change: a pane is now the plugin's REAL panel element,
moved into the pop-out window and still running the plugin's own code
(got-feedback/feedback#928).

That forces one thing here, and it is worth being loud about it:

  WE MUST NOT CREATE THE PANE WINDOW.

To move a live DOM node into another window, the renderer needs a handle on
that window's document. A BrowserWindow we construct in the main process
gives it no such handle. So the renderer opens the window itself with
window.open(), Electron's setWindowOpenHandler turns that into a real
BrowserWindow anyway, and we recognise it in did-create-window by the frame
name the renderer gave it (`fbpane-<paneId>`) and attach the OS behaviour:
remembered bounds, off the taskbar, minimize-to-tray, listed in the tray.

Create the window here instead and the whole feature collapses back into
"reimplement the panel in the pop-out and sync it over IPC" — which is
exactly what we just deleted.

The IPC surface shrinks to two channels, because main never creates or
destroys a pane window and never looks inside one:

  pane:sync    renderer → main   the registry, so the tray can list panes
  pane:toggle  main → renderer   the tray asking for a pane; only the
                                 renderer knows what opening one means (it
                                 might belong in the dock, and its element
                                 lives there)

Gone: pane:open, pane:close, pane:focus, pane:setAlwaysOnTop, pane:closed.
The renderer holds the WindowProxy for a window it opened, so it already
knows when the user closes it — and it has to, because its element is inside
and must be brought home.

Signed-off-by: topkoa <topkoa@gmail.com>
2026-07-12 18:14:49 -04:00
topkoa 39251f3d12 feat(panes): pane pop-out windows + the system tray
feedBack core gained a pane system (window.feedBack.panes): live UI — a
mixer, a camera rig, a readout — authored once and hostable anywhere. In
a browser it pops out via window.open(). This gives it the desktop
treatment: a real BrowserWindow that remembers where you put it, can
float above everything, and lives in the system tray.

First Tray in the app. It exists because a popped-out pane is furniture:
you want it out of the way while you play and back instantly when you
don't — not hunted for behind the main window, and not cluttering the
taskbar. Minimizing a pane sends it to the tray; the tray menu lists
every pane with a checkmark and toggles it.

## The renderer owns the truth

Main never looks inside a pane. It owns OS surfaces only — windows and
their geometry — and learns what panes exist from a `pane:sync` push. The
tray menu is a VIEW of the renderer's registry, not a second copy of it.
When the tray toggles a pane it has no window for, it asks the renderer,
because only the renderer knows what opening one means (it might belong
in the dock).

## The pane window loads OUR origin, and that is load-bearing

A pane is fed over BroadcastChannel, which only reaches windows in the
same Chromium instance and origin. Push the URL anywhere else and the
pane opens looking perfect and never updates again. So `pane:open`
validates the URL against the same origin predicate the navigation guards
use (makeRendererOriginPredicate) and refuses anything else outright —
which also means we can never open arbitrary web content with the full
preload bridge attached. It is the same reason main.ts's
setWindowOpenHandler answers same-origin URLs with `allow` rather than
`deny` + openExternal.

## Details that bite

- sanitizeWindowBounds hard-floored at the MAIN window's 800x600. A 380x560
  pane restored through it would be silently inflated threefold. It now takes
  a WindowSizing; the main window passes its old values as the default, so
  every existing call site and the existing test are byte-for-byte unchanged.
- Pane geometry lives in the DESKTOP config, not the renderer's localStorage
  — localStorage is shared with the pane windows themselves (same origin), so
  a second writer there would race. setDesktopConfig merges shallowly, so
  paneWindows is read-modify-written or one pane's save would drop the rest.
- Pane windows are destroyed when the main window closes. Without the
  renderer there is nothing on the other end of their channel, so they would
  sit showing a frozen playhead forever — and a pane HIDDEN in the tray is
  still an open window, which would stop `window-all-closed` from ever firing
  and leave the app running as an invisible process.
- Geometry is persisted on move/resize, not only on close: a pane window can
  outlive the app in a crash, and the entire point is that you never place it
  twice.
- Electron's 'minimize' is not cancellable here (the listener takes no event),
  so a pane hides right after minimizing rather than preventing it. The window
  is skipTaskbar, so there is no animation to see.
- The tray icon is copied to dist/main/ by build:ts, the same trick
  splash.html and spinner.json use — so __dirname resolves it identically in
  dev and inside a packaged asar, with no app.isPackaged branch and nothing
  added to electron-builder's extraResources. An unreadable icon logs and
  skips the tray rather than creating an invisible one whose menu no one can
  ever reach.

Needs the matching core change (got-feedback/feedback#928), which registers
the `desktop` host when this bridge is present and falls back to a browser
pop-up when it isn't. An older core simply never calls these channels.

Signed-off-by: topkoa <topkoa@gmail.com>
2026-07-12 17:22:44 -04:00
OmikronApexandGitHub 72a3f871e5 Merge pull request #102 from got-feedBack/fix/nightly-single-portable-zip
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
fix(nightly): ship a single portable zip on Windows
2026-07-12 23:14:36 +02:00
OmikronApexandClaude Fable 5 ec6a84c3c8 fix(nightly): dedupe publish assets across overlapping globs
The artifacts/**/*.zip glob overlaps artifacts/**/velopack/**/*, so a
zip inside a velopack dir would be uploaded twice and fail the release
create. Track seen paths and append each file once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 23:09:11 +02:00
OmikronApexandClaude Fable 5 a56ef7828c fix(nightly): suppress Velopack Portable.zip on macOS too
The mac vpk pack emitted feedback-osx-arm64-nightly-Portable.zip
alongside the manual tester zip — same duplication as Windows. Updates
use the -full.nupkg feed, so the portable zip is redundant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:54:50 +02:00
OmikronApexandClaude Fable 5 970bd5068e fix(nightly): ship a single portable zip on Windows
vpk pack emitted a Portable.zip (Update.exe stub wrapping win-unpacked)
alongside the plain feedback-windows-x64.zip, so testers saw two
different portable downloads. Pass --noPortable to suppress it and add
artifacts/**/*.zip to the publish globs so the plain zip (and the macOS
tester zip) land on the rolling nightly release instead of only in
workflow artifacts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:51:35 +02:00
OmikronApexandGitHub ce242a6bcf Merge pull request #100 from got-feedBack/fix/loopback-capture-permission
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
fix(main): permission handler was denying whole-app loopback capture
2026-07-12 01:51:29 +02:00
OmikronApexandClaude Fable 5 bcbb0e329e fix(main): allow getDisplayMedia through the media permission handler
getDisplayMedia (renderer-bus whole-app loopback capture) rides the
'media' permission request with EMPTY mediaTypes. The audio-only rule
denied it, so the display-media handler never ran and exclusive/ASIO
output lost all page audio (song previews, element-song fallback).

Allow media requests unless they explicitly ask for 'video' (camera
stays blocked; getDisplayMedia video is the app's own frame), mirror
the policy in the permission-check handler, and add [asio-diag] logs
to every deny path plus the display-media handler so future denials
name their stage in tester logs.

Verified packaged: display-media granted → renderer-bus engaged,
engine busEnabled/busFlowing=true on ASIO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:44:06 +02:00
c71aa7c82f feat(audio): ASIO/exclusive — renderer-bus in streamer mix, loopback plumbing, cache clear (#98)
* feat(audio): renderer-bus in streamer mix + whole-app loopback plumbing

Two tester-confirmed gaps under ASIO/exclusive output:

1. Streamer mix carried guitar only when the song rode the renderer bus:
   composeAndPushStreamMix mixed guitar + native backing, never the bus.
   The bus ring is single-consumer, so the consumer step is reworked from
   mixRendererBusInto (drain+add) to pullRendererBus (drain once into a
   fixed scratch); both output callbacks then share the pulled block
   between the device output and the stream submix (rides includeBacking
   — it IS song audio).

2. Previews/UI sounds bypass the per-surface feeder taps entirely and
   leak to the default WASAPI device (audible under ASIO, which doesn't
   silence that endpoint). New plumbing lets the static bundle capture
   ALL app audio: setDisplayMediaRequestHandler answers with this
   window's own frame as audio source (frame-scoped — no other apps'
   audio), plus audio:setPageMuted IPC + preload setPageMuted() as the
   local-silence fallback when suppressLocalAudioPlayback is unsupported.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(diag): engine health metrics on every [asio-diag] line

Tester symptom: all audio dead after stopping a song with tones active.
The snapshot showed routing state but not whether the engine was still
producing. Append volatile fields (outside change-detection): in/out/
backing levels, bus fill, input overflows, output underflows, split-ring
fill — outputLevel≈0 with running=true is the silent-engine signature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(main): clear Chromium HTTP cache before first load

Testers hop between portable builds sharing one userData dir; an older
build's server sent no Cache-Control, so its cached /static/app.js
outlived it and silently replaced the new build's renderer code (the
fix14 'watcher never installed' log). One cheap clearCache() per launch
makes stale-bundle states impossible even against old-server caches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:22:30 +02:00
6349ed4c5f feat(window): persist main window size/position across launches (#97)
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
* feat(window): persist main window size/position across launches

The main window always opened at a fixed 1400x900, forcing a manual
resize every session. Save the window geometry (normal bounds +
maximized flag) to the existing desktop prefs store on close, and
restore it in createWindow.

Saved bounds are validated by a pure sanitizer against the current
display layout before use, so stale state degrades safely instead of
producing an off-screen or absurd window:
- garbage/partial config -> 1400x900 centered defaults
- size clamped between the 800x600 window minimums and the largest
  display's workArea
- position kept only when the window overlaps a display by at least
  100x50 px (unplugged monitor / resolution change -> re-center);
  negative multi-monitor coordinates remain valid
- maximized sessions save getNormalBounds() and re-maximize on
  restore; fullscreen deliberately restores windowed

No new dependency; reuses get/setDesktopConfig (atomic write,
fail-soft) in soundfont-manager.ts. The store file is already in the
reset-app-settings delete-set, so a config reset also resets bounds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: gionnibgud <gionnibgud@gmail.com>

* fix(window): don't crash shutdown if bounds persistence write fails

The close listener called setDesktopConfig synchronously with no error
handling; a disk-full or permissions failure during the write would throw
unhandled inside the close handler, risking a shutdown crash. Wrap the
write in try/catch and log a warning instead. Flagged by CodeRabbit on PR #97.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: gionnibgud <gionnibgud@gmail.com>

---------

Signed-off-by: gionnibgud <gionnibgud@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 15:41:47 +02:00
OmikronApexandGitHub f66c27b459 Merge pull request #95 from got-feedBack/diag/asio-routing-debug
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
feat(diag): --debug ASIO routing diagnostics ([asio-diag])
2026-07-11 00:34:36 +02:00
OmikronApexandClaude Fable 5 d069414300 fix(diag): compute busFlowing from per-poll counter deltas
Cumulative pushed/consumed never regress, so '> 0' stayed true forever
after the first frame — a stalled renderer bus (one of the states this
diagnostic exists to expose) would still report flowing. First sample
after enable reports false (no baseline yet).

Addresses CodeRabbit review on #95.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 00:18:10 +02:00
OmikronApexandClaude Fable 5 ee7abe3dc9 feat(diag): --debug ASIO routing diagnostics ([asio-diag])
Testers report song/game audio on the default WASAPI device while the
guitar rides the selected ASIO output. The renderer-side reroute chain
([feedpak-route]/[renderer-bus]) has change-gated logs, but the native
side of the story (what the engine actually opened, whether the backing
transport/renderer bus is live) was invisible in tester logs.

- audio-bridge: debug-gated 2s engine snapshot, change-gated + 30s
  heartbeat: device types/names, duplex, backing, renderer-bus health
- debug:isEnabled IPC + preload audio.debugEnabled() so the static
  bundle can gate its verbose lines on --debug

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 00:08:33 +02:00
OmikronApexandGitHub 8fa28cf2c2 Merge pull request #94 from got-feedBack/fix/nightly-velopack-entitlements
fix(ci): complete #93 — vpk .entitlements rename + core bundle fixes
2026-07-10 21:03:08 +02:00
OmikronApexandClaude Fable 5 09617aa417 fix(bundle): copy all top-level core python packages (routers/ etc.)
core#834 extracted routes into a new top-level routers/ package; the
bundle script only copied lib/, so the packaged backend would die with
ModuleNotFoundError: No module named 'routers'. Copy every top-level
package (dir with __init__.py) except plugins/ (bundled selectively
below) and tests/ (never ships), so the ongoing R3 extraction series
can't keep breaking packaged builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:47:32 +02:00
OmikronApexandClaude Fable 5 add4facb85 fix(bundle): copy all top-level core .py modules, not just server.py
Core added appstate.py (core#833); bundle-slopsmith.sh's whitelist only
copied server.py, so packaged builds crashed on startup with
ModuleNotFoundError: No module named 'appstate'. Glob top-level *.py so
future sibling modules can't repeat this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:22:05 +02:00
OmikronApexandClaude Fable 5 83b8b66fe7 fix(ci): rename entitlements file — vpk requires .entitlements extension
Nightly run 29093119570 failed:
  --signEntitlements does not have an .entitlements extension
vpk validates the file extension. Rename
resources/entitlements.mac.plist -> entitlements.mac.entitlements
and update all references (electron-builder config, sign-macos-binaries.sh,
both workflows). Content unchanged; codesign/electron-builder accept any
extension.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:04:31 +02:00
OmikronApexandGitHub eca559de76 Merge pull request #93 from got-feedBack/fix/nightly-velopack-entitlements
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
fix(ci): preserve mac entitlements through Velopack re-sign
2026-07-10 14:36:20 +02:00
OmikronApexandClaude Fable 5 1046b71f34 fix(ci): anchor --signEntitlements path with $PWD
vpk signing docs recommend absolute paths; relative path breaks if vpk
changes cwd internally. CodeRabbit review nit on #93.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 14:18:47 +02:00
OmikronApexandClaude Fable 5 077309cf00 fix(build): preserve mac entitlements through Velopack re-sign
Same gap as nightly.yml: vpk pack --signAppIdentity without
--signEntitlements strips electron-builder's entitlements
(audio-input, allow-jit, ...) from the release feed's .app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 14:04:45 +02:00
OmikronApexandClaude Fable 5 782aea76f9 fix(nightly): preserve mac entitlements through Velopack re-sign
vpk pack re-signs the .app; without --signEntitlements the codesign
strips electron-builder's entitlements (com.apple.security.device.audio-input
et al). Hardened-runtime app then gets microphone auto-denied by TCC with
no prompt — root cause of silent-input nightly reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 14:03:15 +02:00
OmikronApexandGitHub bcbc7963d3 Merge pull request #92 from got-feedBack/fix/mac-tcc-diagnostic-logging
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
fix(mac): add diagnostic logging to microphone TCC permission flow
2026-07-10 00:11:14 +02:00
OmikronApexandClaude 0645ce724d fix(mac): add diagnostic logging to ensureMicrophoneAccess TCC flow
Every decision point in the macOS microphone permission path now logs its
status and context to the debug log, including the previously silent
'granted' early-return path — which is the prime suspect for the stale-
grant bug (TCC reports 'granted' for a signature-keyed entry that no
longer matches the running binary).

Also logs:
- platform gate skip
- app.isPackaged === false skip (and why it matters re: NSMicrophoneUsageDescription)
- getMediaAccessStatus return value in all branches
- app identity (name, version, exe path) on 'granted' early return
- askForMediaAccess result + extra warning on user denial
- full error stack on exceptions (not just the message)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-09 23:54:20 +02:00
31248617a5 feat(audio): renderer-audio bus — mix renderer WebAudio master into engine output (Phase 2) (#91)
* fix(audio): prefer same-backend duplex routing

* fix(audio): centre mono input; limit duplex to same-endpoint devices

Two issues found while testing the USB-guitar-cable path on Windows:

1. Centre a mono input. SourceChain::processBlock fell into the
   pass-through branch for a 1-channel input, filling only
   min(inputChannels, outputChannels) = 1 output channel and zeroing the
   rest, so a mono USB guitar cable played out of the left speaker only.
   A single-channel input is now broadcast across every output channel.

2. Only attempt the combined (duplex) device when input and output are
   the SAME physical endpoint. Two different endpoints of the same
   backend (USB cable in + separate speakers out) are independent
   hardware clocks; routing them through one duplex device was unstable
   across the app lifecycle (no audio until an explicit Apply, then
   distortion / dropouts / silent-in-song on navigation). Different
   endpoints now use the split path, whose ring bridges the two clocks.
   Same-endpoint duplex (one interface for in and out) keeps the
   low-latency win. Low latency for the two-device case is a follow-up
   that needs the device-lifecycle work (startup restore + reconfigure
   on navigation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu

* fix(audio): probe same-endpoint duplex the same way apply routes it

Startup auto-apply (renderer init) fail-closes on probeDeviceOptionsDual's
`compatible` verdict, but the probe still measured a COMBINED duplex device
for any same-backend pair while setAudioDevices now opens split for
different endpoints. That mismatch made the startup probe describe a config
that isn't the one applied — surfacing as "no audio until I press Apply" for
a USB cable + separate speakers. Gate the probe's duplex path on the same
sameEndpointIntent (same type AND same device) the apply path uses, so a
two-device pair is probed via the split path it will actually run on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu

* fix(audio): never feed chain processors blocks larger than prepared size

WASAPI shared mode can deliver oversized blocks right after a device
start. The NAM core pre-allocates its conv ring/output buffers to the
Reset() maxBufferSize and only asserts (release no-op) on larger blocks;
one oversized block corrupts the conv ring state and garbles all
subsequent audio until the next Reset() — the 'first start heavily
distorted until tone reset / engine restart' bug.

- NAMProcessor::processBlock: process in slices of at most the prepared
  block size.
- SignalChain::process: slice oversized device blocks into prepared-size
  chunks before any slot (VST/NAM/IR) sees them.

See docs/audio-distortion-first-start-investigation.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(audio): close stale-format race when processors are added mid-reconfigure

addProcessor/replaceProcessor prepare the incoming processor off the
audio lock on N-API worker threads. A concurrent device reconfigure's
SignalChain::prepare() can't see that processor (not slotted yet), so a
slot could go live prepared at a stale sample rate / block size and stay
wrong until the next device restart — heard as pitch-shifted/garbled
monitoring when a chain loads while the device is being (re)opened
(widest window: WASAPI exclusive mode's slower open).

Re-check the chain's current format under the lock at insert/swap time
and re-prepare if it moved; log the transition to stderr so tester logs
show when the race fired. prepare() now publishes the format under the
lock so the check can't tear.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(effects): align executor plan schema with rebranded capability layer

The rebrand left a three-way schema split: rig_builder sent the old
'slopsmith.audio_effects.chain_plan.v1', the renderer capability layer
validated against the new 'feedBack.…' id (rejecting every plan), and
this executor still expected the old one. Result: every song chain load
fell back to legacy clearChain+loadPreset — a full multi-VST rebuild per
currentSong poll cycle, heard as continuous distortion during playback
(tester logs: 4-6 rebuilds/session, slot IDs into the 90s).

Executor now uses the rebranded id and accepts the legacy one as an
alias (matching the capability layer's new alias), so neither side of
the handoff can break on old plugin bundles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(audio): guard input callback against double registration

Tester main-process log showed two consecutive 'startAudio: duplex=0'
lines: a transient audioDeviceStopped() (WASAPI exclusive opens fire one
mid-start) cleared audioRunning while the input callback stayed
attached, so the second startAudio() re-added it. JUCE then dispatched
the input callback twice per block: DSP ran twice and each block was
pushed into the split ring twice — every sample played twice (half
speed, one octave down, garbled). stopAudio()'s single
removeAudioCallback left the duplicate registration alive, wedging the
engine (restart no longer helped) and keeping the exclusive-mode device
open even after app close.

Mirror the existing outputCallbackRegistered guard for the input side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(audio): diagnostic instrumentation for tester repro builds

Main-process stderr logging on every open lead, RT paths rate-limited
(first-25 per anomaly + ~5s heartbeats per callback clock):

- primary callback re-entrancy (duplicate registration detector)
- oversized blocks on primary/output callbacks, SignalChain slicing,
  NAM chunking (pre-fix corruption trigger visibility)
- ring fill + under/overflow counters (split-mode pacing)
- device lifecycle: aboutToStart/stopped on both managers with sr/bs and
  callback-registration flags; startAudio guard-skip; stopAudio state
- SourceChain.prepare format trace (stale-rate lead)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(audio): drop diagnostic heartbeats, keep anomaly detectors

The 5s ring/format heartbeats served the distortion hunt and are noise
now. Keep the cheap anomaly-only diagnostics (callback re-entrancy,
oversized-block detectors, chain slicing/chunking, stale-format
re-prepare, device lifecycle) — they log only on misbehavior and stay
relevant for the exclusive-mode playback work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: keep investigation notes out of the PR (local working notes)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: pin JUCE WASAPI exclusive device-type name

The shared player bundle (feedBack#824) detects exclusive-style output
by string-matching getCurrentDevice().outputType. The name is hardcoded
in vendored JUCE; a JUCE upgrade renaming it would silently disable the
feedpak-under-exclusive routing. Fail the build instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(audio): renderer-audio bus — mix renderer WebAudio master into engine output (Phase 2)

SPSC packed-LR ring (64K frames) fed over IPC by the renderer, consumed by
whichever output callback is live (duplex or split), mixed like a backing
track before master gain. Producer-side linear resampling with cross-chunk
continuity; drop-oldest on overflow. Prefill gate (~10.7 ms) and fill clamp
(~85 ms → trim to prime target) added from fix12 tester spike data, which
also confirmed clock stability (drift → 0, zero overflow over 8 min).
Off by default — zero behavior change until the renderer enables it.

Exposed as setRendererBus / pushRendererAudio (fire-and-forget IPC,
~100 msgs/s) / getRendererBusMetrics. Spike script included for tester
go/no-go runs. Consumed by the feeder in feedBack#824's Phase 2 follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(audio): gate lifecycle [diag] logs behind SLOPSMITH_SANDBOX_DEBUG

Review follow-up (#86): the six lifecycle diagnostics (stopAudio,
audioDeviceAboutToStart/Stopped, audioOutputAboutToStart/Stopped,
SourceChain::prepare) printed unconditionally while the PR body claimed
they were verbose-gated. Gate them behind the existing
slopsmith_vst_trace::isEnabled() runtime flag (SLOPSMITH_SANDBOX_DEBUG —
already flipped by the app's debug-logging switch, so tester debug runs
still capture them). The RT-path anomaly detectors (primary re-entry,
oversized-block) keep their firstN/anomaly bounds unchanged, as reviewed.

VSTTrace.h now defines NOMINMAX/WIN32_LEAN_AND_MEAN before windows.h so
including it from engine TUs doesn't clobber std::min.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:03:31 +02:00
2d0dd12abe fix(audio): input-callback double registration, stale-format race, effects schema alignment (#86)
* fix(audio): prefer same-backend duplex routing

* fix(audio): centre mono input; limit duplex to same-endpoint devices

Two issues found while testing the USB-guitar-cable path on Windows:

1. Centre a mono input. SourceChain::processBlock fell into the
   pass-through branch for a 1-channel input, filling only
   min(inputChannels, outputChannels) = 1 output channel and zeroing the
   rest, so a mono USB guitar cable played out of the left speaker only.
   A single-channel input is now broadcast across every output channel.

2. Only attempt the combined (duplex) device when input and output are
   the SAME physical endpoint. Two different endpoints of the same
   backend (USB cable in + separate speakers out) are independent
   hardware clocks; routing them through one duplex device was unstable
   across the app lifecycle (no audio until an explicit Apply, then
   distortion / dropouts / silent-in-song on navigation). Different
   endpoints now use the split path, whose ring bridges the two clocks.
   Same-endpoint duplex (one interface for in and out) keeps the
   low-latency win. Low latency for the two-device case is a follow-up
   that needs the device-lifecycle work (startup restore + reconfigure
   on navigation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu

* fix(audio): probe same-endpoint duplex the same way apply routes it

Startup auto-apply (renderer init) fail-closes on probeDeviceOptionsDual's
`compatible` verdict, but the probe still measured a COMBINED duplex device
for any same-backend pair while setAudioDevices now opens split for
different endpoints. That mismatch made the startup probe describe a config
that isn't the one applied — surfacing as "no audio until I press Apply" for
a USB cable + separate speakers. Gate the probe's duplex path on the same
sameEndpointIntent (same type AND same device) the apply path uses, so a
two-device pair is probed via the split path it will actually run on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu

* fix(audio): never feed chain processors blocks larger than prepared size

WASAPI shared mode can deliver oversized blocks right after a device
start. The NAM core pre-allocates its conv ring/output buffers to the
Reset() maxBufferSize and only asserts (release no-op) on larger blocks;
one oversized block corrupts the conv ring state and garbles all
subsequent audio until the next Reset() — the 'first start heavily
distorted until tone reset / engine restart' bug.

- NAMProcessor::processBlock: process in slices of at most the prepared
  block size.
- SignalChain::process: slice oversized device blocks into prepared-size
  chunks before any slot (VST/NAM/IR) sees them.

See docs/audio-distortion-first-start-investigation.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(audio): close stale-format race when processors are added mid-reconfigure

addProcessor/replaceProcessor prepare the incoming processor off the
audio lock on N-API worker threads. A concurrent device reconfigure's
SignalChain::prepare() can't see that processor (not slotted yet), so a
slot could go live prepared at a stale sample rate / block size and stay
wrong until the next device restart — heard as pitch-shifted/garbled
monitoring when a chain loads while the device is being (re)opened
(widest window: WASAPI exclusive mode's slower open).

Re-check the chain's current format under the lock at insert/swap time
and re-prepare if it moved; log the transition to stderr so tester logs
show when the race fired. prepare() now publishes the format under the
lock so the check can't tear.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(effects): align executor plan schema with rebranded capability layer

The rebrand left a three-way schema split: rig_builder sent the old
'slopsmith.audio_effects.chain_plan.v1', the renderer capability layer
validated against the new 'feedBack.…' id (rejecting every plan), and
this executor still expected the old one. Result: every song chain load
fell back to legacy clearChain+loadPreset — a full multi-VST rebuild per
currentSong poll cycle, heard as continuous distortion during playback
(tester logs: 4-6 rebuilds/session, slot IDs into the 90s).

Executor now uses the rebranded id and accepts the legacy one as an
alias (matching the capability layer's new alias), so neither side of
the handoff can break on old plugin bundles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(audio): guard input callback against double registration

Tester main-process log showed two consecutive 'startAudio: duplex=0'
lines: a transient audioDeviceStopped() (WASAPI exclusive opens fire one
mid-start) cleared audioRunning while the input callback stayed
attached, so the second startAudio() re-added it. JUCE then dispatched
the input callback twice per block: DSP ran twice and each block was
pushed into the split ring twice — every sample played twice (half
speed, one octave down, garbled). stopAudio()'s single
removeAudioCallback left the duplicate registration alive, wedging the
engine (restart no longer helped) and keeping the exclusive-mode device
open even after app close.

Mirror the existing outputCallbackRegistered guard for the input side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(audio): diagnostic instrumentation for tester repro builds

Main-process stderr logging on every open lead, RT paths rate-limited
(first-25 per anomaly + ~5s heartbeats per callback clock):

- primary callback re-entrancy (duplicate registration detector)
- oversized blocks on primary/output callbacks, SignalChain slicing,
  NAM chunking (pre-fix corruption trigger visibility)
- ring fill + under/overflow counters (split-mode pacing)
- device lifecycle: aboutToStart/stopped on both managers with sr/bs and
  callback-registration flags; startAudio guard-skip; stopAudio state
- SourceChain.prepare format trace (stale-rate lead)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(audio): drop diagnostic heartbeats, keep anomaly detectors

The 5s ring/format heartbeats served the distortion hunt and are noise
now. Keep the cheap anomaly-only diagnostics (callback re-entrancy,
oversized-block detectors, chain slicing/chunking, stale-format
re-prepare, device lifecycle) — they log only on misbehavior and stay
relevant for the exclusive-mode playback work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: keep investigation notes out of the PR (local working notes)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(audio): gate lifecycle [diag] logs behind SLOPSMITH_SANDBOX_DEBUG

Review follow-up (#86): the six lifecycle diagnostics (stopAudio,
audioDeviceAboutToStart/Stopped, audioOutputAboutToStart/Stopped,
SourceChain::prepare) printed unconditionally while the PR body claimed
they were verbose-gated. Gate them behind the existing
slopsmith_vst_trace::isEnabled() runtime flag (SLOPSMITH_SANDBOX_DEBUG —
already flipped by the app's debug-logging switch, so tester debug runs
still capture them). The RT-path anomaly detectors (primary re-entry,
oversized-block) keep their firstN/anomaly bounds unchanged, as reviewed.

VSTTrace.h now defines NOMINMAX/WIN32_LEAN_AND_MEAN before windows.h so
including it from engine TUs doesn't clobber std::min.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:44:15 +02:00
91c4e0037f test: pin JUCE WASAPI exclusive device-type name (#90)
The shared player bundle (feedBack#824) detects exclusive-style output
by string-matching getCurrentDevice().outputType. The name is hardcoded
in vendored JUCE; a JUCE upgrade renaming it would silently disable the
feedpak-under-exclusive routing. Fail the build instead.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:18:04 +02:00
b2be91808c fix(audio): accept standard base64 for IR/NAM slot state (was silently dropped) (#88)
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
MemoryBlock::fromBase64Encoding only parses JUCE's proprietary
"<size>.<alphabet>" format and returns false on standard RFC-4648 base64 —
which is what the Python-side plugins (rig_builder) emit for per-slot state.
That silent false meant LoadPresetWorker/SetSlotState never called setState()
for those slots, so IR stages lost their per-stage `gain` (cab loudness makeup,
amp-trim impulse compensation) on every chain load.

Add decodeStateBlob(): JUCE format first (engine-native saves unchanged), then
a standard-base64 fallback via juce::Base64 — gated to IR/NAM slots only, whose
processors take exactly the JSON these states carry ({"irPath","gain"} /
{"modelPath",...}). VST slots keep the JUCE-only decode: their plugin-emitted
blobs are metadata wrappers, not real setStateInformation() chunks.

Co-authored-by: Jafz2001 <ignacio.fritis@mundotelecomunicaciones.cl>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:14:14 +02:00
3e3f1f868c fix(audio): never feed chain processors blocks larger than prepared size (#85)
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
* fix(audio): never feed chain processors blocks larger than prepared size

WASAPI shared mode can deliver oversized blocks right after a device
start. The NAM core pre-allocates its conv ring/output buffers to the
Reset() maxBufferSize and only asserts (release no-op) on larger blocks;
one oversized block corrupts the conv ring state and garbles all
subsequent audio until the next Reset() — the 'first start heavily
distorted until tone reset / engine restart' bug.

- NAMProcessor::processBlock: process in slices of at most the prepared
  block size.
- SignalChain::process: slice oversized device blocks into prepared-size
  chunks before any slot (VST/NAM/IR) sees them.

See docs/audio-distortion-first-start-investigation.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(audio): prefer same-backend duplex routing

* fix(audio): centre mono input; limit duplex to same-endpoint devices

Two issues found while testing the USB-guitar-cable path on Windows:

1. Centre a mono input. SourceChain::processBlock fell into the
   pass-through branch for a 1-channel input, filling only
   min(inputChannels, outputChannels) = 1 output channel and zeroing the
   rest, so a mono USB guitar cable played out of the left speaker only.
   A single-channel input is now broadcast across every output channel.

2. Only attempt the combined (duplex) device when input and output are
   the SAME physical endpoint. Two different endpoints of the same
   backend (USB cable in + separate speakers out) are independent
   hardware clocks; routing them through one duplex device was unstable
   across the app lifecycle (no audio until an explicit Apply, then
   distortion / dropouts / silent-in-song on navigation). Different
   endpoints now use the split path, whose ring bridges the two clocks.
   Same-endpoint duplex (one interface for in and out) keeps the
   low-latency win. Low latency for the two-device case is a follow-up
   that needs the device-lifecycle work (startup restore + reconfigure
   on navigation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu

* fix(audio): probe same-endpoint duplex the same way apply routes it

Startup auto-apply (renderer init) fail-closes on probeDeviceOptionsDual's
`compatible` verdict, but the probe still measured a COMBINED duplex device
for any same-backend pair while setAudioDevices now opens split for
different endpoints. That mismatch made the startup probe describe a config
that isn't the one applied — surfacing as "no audio until I press Apply" for
a USB cable + separate speakers. Gate the probe's duplex path on the same
sameEndpointIntent (same type AND same device) the apply path uses, so a
two-device pair is probed via the split path it will actually run on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu

* fix(audio): close stale-format race when processors are added mid-reconfigure

addProcessor/replaceProcessor prepare the incoming processor off the
audio lock on N-API worker threads. A concurrent device reconfigure's
SignalChain::prepare() can't see that processor (not slotted yet), so a
slot could go live prepared at a stale sample rate / block size and stay
wrong until the next device restart — heard as pitch-shifted/garbled
monitoring when a chain loads while the device is being (re)opened
(widest window: WASAPI exclusive mode's slower open).

Re-check the chain's current format under the lock at insert/swap time
and re-prepare if it moved; log the transition to stderr so tester logs
show when the race fired. prepare() now publishes the format under the
lock so the check can't tear.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
2026-07-08 22:44:26 +02:00
56e929da4e audio: replaceIR(slotId, path, gain) for in-place cab/IR swap (#83)
* audio: add replaceIR(slotId, path, gain) for in-place cab/IR swap

Swap an existing convolution slot's IR without a full loadPreset, so the rest
of the chain — the amp VST above all — is not torn down and rebuilt (that
teardown is the ~1-2 s wait when changing cabs / mic position). Mirrors the
existing loadIR worker but calls SignalChain::replaceProcessor(slotId, ...);
optional gain updates the slot post-gain (the cab makeup).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* audio: expose replaceIR over the IPC bridge

Wire the native replaceIR(slotId, path, gain) through audio-bridge (ipcMain
handle) + preload, so renderers get feedBackDesktop.audio.replaceIR. Lets the
rig-builder cab room swap a cab's IRs in place instead of a full loadPreset +
param re-apply (that re-apply was the brief 'can't move the mic yet' lag).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* audio: replaceIR updates slot name/path so getChainState reflects the swap

replaceProcessor deliberately preserves the target slot's name/path during
the prepare/fault window (so a fault in prepareToPlay is blocklisted against
the right plugin path). It kept them even on success, so after a successful
replaceIR the slot's audio was the new IR but getChainState()/preset-save
still reported the OLD IR name+path — a footgun for any consumer that
persists a chain read back from getChainState().

Add optional newName/newPath to replaceProcessor, applied under the swap lock
ONLY on success (empty = keep, so the sandbox-promotion caller is unchanged).
ReplaceIRWorker passes "IR: <name>" + the new path, mirroring LoadIRWorker.

Built (npm run build:audio) clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Jafz2001 <ignacio.fritis@mundotelecomunicaciones.cl>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
2026-07-08 22:20:06 +02:00
OmikronApexandGitHub a53fd38732 Merge pull request #84 from got-feedBack/chore/plugin-manager-rebrand
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
chore(plugin-manager): rebrand UI strings from Slopsmith to fee[dB]ack
2026-07-08 09:55:58 +02:00
OmikronApexandClaude Fable 5 f3c14271cc chore(plugin-manager): rebrand UI strings from Slopsmith to fee[dB]ack
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:22:46 +02:00
OmikronApexandGitHub ac824d5cc5 Merge pull request #81 from got-feedBack/chore/drop-setlist-plugin
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
chore(build): stop bundling feedback-plugin-setlist
2026-07-07 19:20:30 +02:00
OmikronApexandClaude Fable 5 524c2e0dca chore(build): stop bundling feedback-plugin-setlist
Playlists are integrated into the core app, so the setlist plugin is
obsolete and its repo is being archived.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 19:17:54 +02:00
dd7ad9b786 feat(update): add nightly Velopack update channel (#80)
Addon CI / addon (arm64, macos-14, mac) (push) Waiting to run
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Waiting to run
Addon CI / addon (x64, windows-latest, win) (push) Waiting to run
Ship CI / CI (push) Waiting to run
Adds `nightly` as a selectable auto-update channel (Windows + macOS).

Client:
- update-manager.ts: add 'nightly' to UpdateChannel (veloChannel already
  yields win-x64-nightly / osx-arm64-nightly, so no logic change)
- main.ts: allow 'nightly' in the runtime IPC channel guard
- preload.ts: add 'nightly' to the preload-local UpdateChannel union
- screen.js / settings.html: add the Nightly option + helper text

CI (nightly.yml):
- derive <pkg>-nightly.<UTC date> version in the setup job
- setup-dotnet (pinned from .build-config.json) so the vpk CLI has net8,
  matching build.yml
- vpk pack win-x64-nightly / osx-arm64-nightly (mac signed + notarized,
  mirroring build.yml's signed/unsigned fallback)
- publish a rolling `nightly` GitHub Release (prerelease=false, latest=false)
  that the in-app updater reads for the nightly channel
- concurrency guard so an overlapping dispatch can't race the rolling release

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 00:10:03 +02:00
OmikronApexandGitHub 34e1aa099d Change nightly workflow schedule to 23:00
Addon CI / addon (arm64, macos-14, mac) (push) Has been cancelled
Addon CI / addon (x64, ubuntu-22.04, linux) (push) Has been cancelled
Addon CI / addon (x64, windows-latest, win) (push) Has been cancelled
Ship CI / CI (push) Has been cancelled
2026-07-05 22:48:36 +02:00
59145e4663 fix(library): create the default library folder on first run (#79)
The Python server only seeds bundled starter content (and scans) when
DLC_DIR.is_dir() is true, and it can't bootstrap the folder itself — the
seed's mkdir runs only after _get_dlc_dir() already resolves a directory. On a
fresh install the default library path didn't exist, so the scan bailed with
"DLC folder not configured" and starter content never seeded.

Create the resolved DLC dir in startPython() before spawning the server so the
first scan seeds the bundled songs. Also modernize the default library path to
~/.local/share/feedback/library, keeping the legacy slopsmith paths as
fallbacks so existing installs that relied on the default keep their populated
library.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.3.0-alpha.1
2026-07-03 23:42:01 +02:00