Commit Graph

172 Commits

Author SHA1 Message Date
Thales Pereira 0ba022b24c fix(player): place loop time inputs right of the loop button
Move the exact loop start/end fields inline into .footer-transport, directly
after the loop button, instead of a separate row below the time readout.
Drop the redundant LOOP label now that the fields sit next to the loop control.
2026-07-10 20:26:23 +01:00
Thales Pereira 9768072193 feat(player): exact timestamp input for loop start/end (#246)
Add two editable timestamp fields in the transport footer for setting the
loop region precisely, alongside the existing drag/click select. Fields
display mm:ss.mmm and accept either mm:ss.mmm or plain decimal seconds.

- utils.js: fmtTimeMs (integer-ms math, no rounding carry) and parseTimecode
  (mm:ss.mmm or plain seconds, null on invalid).
- transport.js: syncLoopInputs keeps the fields in sync on drag/toggle
  (never clobbering a field being edited, disabled when no track loaded);
  commitLoopInput parses, clamps to [0, totalDuration], enforces the
  MIN_LOOP_SEC ordering, then updates the loop via the existing setters +
  updateLoopRegionVisual. Enter/blur commit, Escape reverts. Invalid input
  reverts the field in place (showError belongs to the import form).
- player.js: refresh loop UI on track load so the inputs enable + reset once
  the duration is known.

Values flow through the existing loopStart/loopEnd setters and
audioEngine.setLoop, so the model and engine are unchanged.
2026-07-06 23:56:55 +01:00
Tha.Les ec73f55d48 fix(desktop): Windows FFmpeg from BtbN GitHub + detect bin/ layout (#248) (#249)
Switch the Windows FFmpeg download from gyan.dev (single US mirror, ~0.1 MB/s
for the reporter) to BtbN's GitHub build, served via GitHub's CDN. Uses the
pinned asset ffmpeg-n8.1-latest-win64-gpl-8.1.zip and verifies it against
BtbN's combined checksums.sha256 (one file listing every asset), replacing the
old per-file {url}.sha256 companion fetch. Fails closed if the archive is not
listed. Custom STEMDECK_FFMPEG_URL overrides still skip verification.

Also detect a manually-placed FFmpeg under data/ffmpeg/bin/ (the upstream
folder layout) in addition to the flat data/ffmpeg/ location, so users who drop
in their own build are honored instead of triggering a download. Detection now
flows through resolve_existing_ffmpeg across probe_runtime, ffmpeg_dir_if_present,
and ensure_ffmpeg; write_setup_config records ffprobe next to the resolved
ffmpeg.
v0.8.0-alpha.5
2026-07-06 17:50:55 +01:00
Thales Pereira 7a15a395e1 ci: remove append_body from OS release workflows - artifact sections now static 2026-06-30 14:21:24 +01:00
Thales Pereira 9dec0e146b fix(mobile): tape-effect fallback for speed on LAN HTTP (no AudioWorklet) v0.8.0-alpha.4 2026-06-30 12:08:15 +01:00
Tha.Les c603c596ae fix(mobile): fast track load + pitch-preserving speed on chunked engine (#242)
Three changes to chunkedAudioEngine.js:

- CHUNK_SEC 10 -> 5: halves the initial chunk download (~10 MB -> ~5 MB for 6
  stems), reducing the first-play buffering time.

- ready() no longer blocks on chunk 0 download. It resolves after the parallel
  WAV header fetches (~6 x 1 KB) and kicks chunk 0/1 off in the background.
  play() already handled the not-yet-cached case, so first play is gapless
  once the background fetch finishes. Track appears ready in the UI in ~100 ms
  instead of several seconds.

- Add SoundTouch WSOLA AudioWorklet on the master bus (same pattern as
  audioEngine.js) and expose setPlaybackRate(). The mobile speed slider was
  already wired to call this method, but it was missing from the API so the
  control silently did nothing. Pitch is now preserved at all speeds on mobile.
2026-06-30 12:03:20 +01:00
Tha.Les 2a40dc504e feat(player): playback speed control with pitch preservation (#241)
* feat(player): playback speed control (0.5x to 2.0x)

Adds a speed slider to the transport footer (desktop) and mixer tab
(mobile) so users can slow down or speed up tracks for practice.

- audioEngine: store _playbackRate, apply to new AudioBufferSourceNodes
  on startSources(), and fix getCurrentTime() to account for rate so
  the waveform playhead and loop detection stay accurate at non-1x speeds
- transport: applySpeed() propagates rate to both engine and streaming
  paths; scroll-wheel support (+-0.25 per tick); double-click resets to 1x
- state: playbackSpeed variable + setter; speedEl/speedLabelEl DOM refs
- player: resetSpeed() called in destroyPlayer() so a new track always
  starts at 1x
- mobile: speed slider in mixer transport section, state.speed reset on
  track open

* fix(player): move speed control below play button, centered

* fix(player): tempo bar full-width below transport, TEMPO label + gold slider

* fix(player): center 1.0x on tempo slider (range 0-2, midpoint = 1.0)

* feat(audio): pitch-preserving tempo via SoundTouch AudioWorklet

Voices and instruments no longer pitch-shift when changing playback speed.
A WSOLA time-stretcher runs as an AudioWorkletProcessor on the master bus
so a single node handles all stems. Falls back to tape-effect if the worklet
API is unavailable.

* fix(audio): close array literal in Promise.all ([]) was missing ]
2026-06-30 11:43:50 +01:00
Tha.Les d61be8c9c3 fix(desktop): Blackwell GPU (RTX 5000) falls back to CPU silently (#239) (#240)
Three changes to address NVIDIA RTX 5000 series (sm_120, cu128) running
stem separation on CPU instead of GPU:

- Bump cu128 torch from 2.7.1 to 2.8.0: 2.7.1 shipped incomplete sm_120
  kernels for Blackwell, causing verify_cuda_torch to fail. 2.8.0+cu128
  wheels are available and include full sm_120 support.

- Log verify_cuda_torch stderr to logs/setup.log: previously silenced
  with Stdio::null(), so there was no diagnostic path when Blackwell
  kernel verification failed.

- Show a visible error in setup.js when gpu_detected but cuda_verified
  is false: the previous status-line message was easy to miss. Now calls
  showError() so the user knows their GPU was found but CUDA setup failed
  and where to look for details.
2026-06-30 08:57:40 +01:00
Tha.Les 32bdc38180 feat(settings): QR codes for network access (#238)
* feat(settings): QR codes for network access addresses

When server mode is on, show a scannable QR code for each local IP in
the desktop settings panel. Each QR encodes http://{ip}:{port}/mobile/
so the phone camera opens the mobile UI directly.

- Add segno (pure Python, no PIL) as a new dependency
- Add GET /api/qr?url=... endpoint that returns an SVG QR code
- Render one QR card per LAN address in the network settings section

* feat(settings): remove IP list, blur QR codes with tap-to-reveal

- Drop the yellow IP address chips; the QR label already shows the URL
- QR codes start blurred so a nearby camera app can't scan them
  immediately; tap any card to toggle the blur
- Add a hint line: "Blurred so your camera doesn't get too excited. Tap to reveal."

* fix(settings): increase gap between QR cards

* fix(settings): clip QR blur bleed with overflow hidden wrapper

* fix(settings): accent color border on QR cards

* fix(settings): thicker accent border on QR cards

* fix(settings): box-sizing border-box on QR wrap to stop corner clipping

* fix(settings): advanced pane scrolls so Done footer stays fixed at bottom
v0.8.0-alpha.3
2026-06-29 19:19:59 +01:00
Tha.Les 52197aa25c feat(mobile): chunked audio engine via WAV Range requests (#237)
Replaces full-file MP3 decode with a progressive WAV engine that fetches
stems in 10-second chunks and chains AudioBufferSourceNodes back-to-back.

- First audio after ~7 MB download (one chunk for 4 stems) vs. waiting
  for the complete file
- Peak RAM ~28 MB vs. ~420 MB for a 5-minute 4-stem track
- No track-length cap (removes the 7-14 min OOM limit)
- Same glitch-free behavior on Safari/WKWebView: AudioBufferSourceNode,
  no streaming elements, no HTTP/1.1 connection-cap underruns
- Backend needs no changes: Starlette FileResponse handles Range requests

Closes #236
2026-06-29 17:00:20 +01:00
Tha.Les 0e25f15c42 fix(mobile): guard against OOM crash on Load, fix stuck Preparing audio (#235)
* fix(mobile): guard against OOM crash on Load, fix stuck Preparing audio

Two bugs reported in #234 (via discussion #216):

1. Mobile browsers crash (WebKit tab kill) when loading long tracks because
   createAudioEngine decodes all stems into AudioBuffers in parallel. Added
   an estimateDecodedBytes check before creating the engine: tracks whose
   decoded PCM would exceed 200 MB (approx 4.5 min x 4 stems) now surface a
   clear error instead of silently crashing the tab.

2. After a crash-induced reload, loadLibrary auto-selected state.tracks[0]
   without calling openTrack(), leaving the player stuck on "Preparing audio"
   indefinitely. Removed the auto-select: the library is visible on load and
   the user can pick a track explicitly.

Closes #234

* fix(mobile): replace OOM hard error with streaming engine fallback

Instead of blocking long tracks with an error, tracks that would exceed
200 MB of decoded PCM (approx 4.5 min x 4 stems) now fall back to a
streaming engine backed by <audio> elements and createMediaElementSource.
No PCM is held in RAM -- the browser streams on demand -- so OOM crashes
are avoided without restricting track length.

The streaming engine implements the same interface as createAudioEngine
(play/pause/seek/setGain/destroy/ready) so openTrack needs no structural
changes beyond selecting which engine to create.

* fix(mobile): play button state color, fix streaming engine seek desync

Play button: add data-playing attribute and CSS so the button turns green
when playing (gold when paused), making the state immediately obvious.

Seek desync: the streaming engine was setting currentTime on all <audio>
elements while they continued playing, causing each stem to arrive at the
new position at a slightly different time. Fix: pause all elements first,
seek all, then resume -- this guarantees all stems restart from the same
position simultaneously.

* feat(mobile): wire prev/next track buttons, match desktop icon button style

Prev/next buttons were rendered but had no data-action and no handler --
clicking them did nothing. Now:

- prevTrack() / nextTrack() navigate state.tracks by index, autoplaying if
  a track was already playing when the button was pressed
- Buttons are disabled at the ends of the library (no prev on first track,
  no next on last track)
- .t-step CSS updated to match the desktop daw-iconbtn style: transparent
  background, rounded corners, hover highlight, scale(0.96) on active

* fix(mobile): proper seek sync and drift correction in streaming engine

Two-part fix for stems going out of sync after dragging the playhead:

1. Seek: pause all elements, set all currentTime, then wait one RAF frame
   for the browser to settle the seeks. Re-read the primary element's
   actual position and align all secondaries to it before calling play().
   This eliminates startup desync caused by elements buffering at different
   rates after a seek.

2. Drift correction: tick() now checks secondary elements every ~1 second
   (60 RAF frames) and snaps any that have drifted more than 50ms back to
   the primary's position. Catches any clock skew that accumulates during
   long playback.

* fix(mobile): redesign prev/next buttons to match standard mobile player style

Remove the rectangular box shape (border-radius:9px) that clashed with the
circular play button. Prev/next are now naked icon buttons with a circular
tap area -- the pattern used by Spotify, Apple Music, etc. Icons enlarged
from 26px to 32px to sit proportionally next to the 66px play button.
Press feedback is a scale+opacity pop instead of a subtle background fill.

* chore(mobile): remove unimplemented speed and loop stub buttons

* fix(mobile): drop streaming engine, use buffer engine for all tracks

The streaming engine (<audio> elements via createMediaElementSource) caused
constant choppy audio -- the exact same HTTP/1.1 connection-cap underrun
issue that audioEngine.js was built to solve in the first place.

Replace with the buffer engine for all tracks. Raise the decoded-PCM limit
from 200 MB to 600 MB, which covers ~14 min x 4 stems at 44.1 kHz/Float32
-- well within the per-tab budget of any post-2019 phone. Tracks over that
threshold get a clear error rather than glitchy playback.

* fix(mobile): correct OOM limit comment and dynamic error message

600 MB / (stems * 2ch * 44100Hz * 4 bytes) = ~7 min for 4 stems, not 14.
The 14-minute figure was only accurate for a 2-stem track. Error message
now computes and shows the real cap based on the actual stem count.

* fix(dev): bind to 0.0.0.0 by default to allow LAN access for mobile testing

* fix(dev): align run.sh default port to 8080 to match settings and desktop
2026-06-29 15:55:09 +01:00
Tha.Les 2cb7214aea fix: server network access and YouTube Shorts support (#233)
* feat: support YouTube Shorts URLs

Normalize youtube.com/shorts/<videoId> to the standard watch?v= form
so yt-dlp receives a URL its extractor already handles.

Adds two test cases covering www. and m. variants.

* fix: allow network access by default in server/Docker mode

Two layers were blocking headless server deployments from accepting
network clients (reported in discussion #216):

1. docker-compose.yml bound to 127.0.0.1:8000 - Docker itself rejected
   connections from the network before they reached the app.

2. _default_allow_network() returned False unconditionally, so the
   network_gate middleware blocked all non-loopback requests even when
   Docker networking was configured correctly.

Fix both: bind the Docker port to 0.0.0.0 and derive the network
default from STEMDECK_DESKTOP - desktop keeps its secure off-by-default
behavior; server/Docker deployments open the gate automatically since
network access is the entire point of a headless deployment.
STEMDECK_ALLOW_NETWORK still takes precedence when set explicitly.

* style: ruff format download.py

* test: update network gate tests for server-mode default

Rename test_default_is_off to clarify it covers desktop mode (now
requires STEMDECK_DESKTOP=1). Add test_default_is_on_in_server_mode
covering the new behavior where allow_network defaults to True when
STEMDECK_DESKTOP is absent.

* fix: hide network and port settings in server/Docker mode

Network toggle and port field are desktop-only controls. In server mode
(no window.__TAURI__) the port is fixed by Docker and network access is
on by default, so exposing these controls is misleading. Hide both from
the Advanced settings tab when not running inside Tauri.

* fix: make network and port settings read-only in server/Docker mode

In server mode (no Tauri) the network toggle is always on and the port
is fixed by Docker, so both controls are shown but disabled so the user
can see the current state without being able to change them.

* fix: add read-only note to server-mode settings

Show a explanatory note at the top of the Advanced tab when running in
server mode so users know the network and port controls are intentionally
locked and where to make changes.
v0.8.0-alpha.2
2026-06-28 20:59:09 +01:00
Tha.Les d9a669c85a feat: mobile UI polish + configurable port (#232)
Follow-ups to the mobile UI (#231):

- Mixer waveform now fills yellow as playback progresses (the played bars,
  not just the playhead), and repaints on seek.
- Library/Mixer/mini-player show the real YouTube/SoundCloud thumbnail when
  available (layered over the gradient as a fallback), not just a letter.
- Configurable port (Settings -> Advanced): default 8080, persisted, read by
  the desktop launcher before spawning the backend (falls back to a free port
  if taken). A stable port means a stable phone URL. Applies on restart.
- Settings General tab: number fields are digit-only text inputs (no spinner
  arrows), length-capped; max track length capped at 20 min with the limit
  noted in the description; controls aligned. Added a Done button.

Co-authored-by: Thales <>
v0.8.0-alpha.1
2026-06-27 20:21:30 +01:00
Tha.Les cde1739c64 feat: mobile web UI + network access toggle (#231)
* feat: mobile web UI + network access toggle

Add a phone-optimized web UI and let other devices on the LAN reach a
StemDeck instance, so the app is usable end-to-end from a phone.

Mobile UI (static/mobile/, vanilla JS to match the stack):
- Library, Mixer, and Extract screens wired to the real API. Library lists
  /api/jobs with swipe-to-delete; Mixer reuses the desktop Web Audio engine
  (audioEngine.js, now accepting a shared gesture-unlocked AudioContext for
  iOS) with faders/mute/solo/seek, real analysis, and mixdown/MP4 export;
  Extract submits URL/upload and follows SSE progress.
- Served by a user-agent check on "/" (phones get mobile, everyone else the
  DAW; ?ui= overrides). Shared DOM-free helpers in static/js/shared/jobs.js.
- Ported from the design prototype kept under design/mobile/.

Network access (app/core/settings.py, app/main.py):
- Backend always binds 0.0.0.0; a runtime gate decides whether non-host
  requests are served (default off, opt-in). The host machine (loopback or
  its own LAN IP) is always allowed, so it can't be locked out.
- Settings dialog reorganized into General / Advanced tabs: General holds
  max track length (<=20 min) and MP4 video quality; Advanced holds the
  network toggle (with the LAN address list) and out-of-sync resync.
- Runtime settings (allow_network, max_duration_sec, video_max_height) are
  persisted and read live via GET/POST /api/settings, no restart needed.

Performance: stem MP3s are transcoded once and cached on disk (was re-encoded
on every request), so loading a track on mobile is fast and re-loads instant.

Desktop: start_backend binds 0.0.0.0; adds a local_ip command.

* chore: address code-quality bot — document suppressed excepts; untrack design refs

- _local_ips() and settings _load()/_save(): replace bare `except: pass` with
  an explanatory comment + logging.debug/warning(exc_info=True); behavior
  unchanged (still best-effort).
- _load(): handle the no-file case explicitly (FileNotFoundError) vs. logging
  genuinely corrupt files.
- Untrack design/ (the imported Claude Design prototype) and gitignore it — it's
  a local spec reference, not shipped code, and the static analyzer's "no-effect
  expression" flags on its <x-dc> template bindings were false positives.

---------

Co-authored-by: Thales <>
2026-06-27 19:04:28 +01:00
dependabot[bot] 8d72f0d684 chore(deps): bump actions/checkout from 6 to 7 (#214)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
v0.7.0-alpha.18
2026-06-26 16:41:14 +01:00
dependabot[bot] c95b42ae48 chore(deps): bump softprops/action-gh-release from 3.0.0 to 3.0.1 (#215)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/b4309332981a82ec1c5618f44dd2e27cc8bfbfda...718ea10b132b3b2eba29c1007bb80653f286566b)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-26 16:23:16 +01:00
Tha.Les 7c39ad1e66 feat: add Empress Effects to We Recommend (+ fix Thomann description) (#230)
* feat: add Empress Effects to We Recommend

Effects-pedal maker; links to empresseffects.com (website, so logo style /
no Instagram glyph, like Lisbon Guitar Works). Image can be dropped at
static/img/friends/empress-effects.png later; shows the monogram until then.

* fix: correct Thomann description to "Online Music Store"

---------

Co-authored-by: Thales <>
2026-06-26 16:04:00 +01:00
Tha.Les cbc64fdfc1 fix: don't auto-purge the desktop library (skip job TTL sweep) (#229)
The desktop app persists its track list permanently in
~/Documents/StemDeck/user-data.json, but stems under jobs/<id>/ were
subject to the 24h job TTL sweep that runs at every startup. After a day
(or any app restart past the TTL -- e.g. installing a new release) the
sweep deleted the stems while the library entries remained, surfacing
"This track's audio is no longer available. Re-upload to restore it."

The TTL is a disk-hygiene default for the shared server/Docker deployment.
On desktop the library is user-curated (folders + Trash), so skip the
sweep when running under the desktop shell (STEMDECK_DESKTOP=1, set by the
Tauri launcher on Windows/macOS/Linux). Disk stays under user control.

Co-authored-by: Thales <>
2026-06-26 15:08:14 +01:00
Tha.Les a45badf3b3 fix(ci): target the macOS runner's default macOS/ARM64 labels (#227)
The self-hosted runner advertises self-hosted/macOS/ARM64 (GitHub's
default macOS labels), but the workflow required a custom 'osx' label
that the runner no longer carries, so every macOS Release since
alpha.15 sat queued with no matching runner. Match the default labels,
consistent with the windows/linux release workflows.

Co-authored-by: Thales <>
2026-06-26 12:28:11 +01:00
Tha.Les 9ca03fc4d1 chore: MP4 wording cleanup + "We Recommend" rename/polish (#228)
* chore: drop "karaoke" wording from the MP4 export

The video export is just an MP4 export, not specifically a karaoke
feature. Replace all "karaoke" references in UI strings, the download
filename, comments, docstrings, and docs with neutral MP4/video wording.
No behavior change.

- UI: MP4 "Export Mix" subtitle -> "Export mix with the original video".
- Download filename: <title>_karaoke.mp4 -> <title>_video.mp4 (frontend
  download attr and backend Content-Disposition).
- Comments / docstrings / README updated; no renamed identifiers
  (downloadCurrentVideo, /video.mp4, has_video were already neutral).

* chore: rename "Supporters" UI label to "We Recommend"

Match the README "We Recommend" section. "Supporters" implied a
sponsorship relationship the project explicitly does not have (no money
or funding accepted); these are editorial recommendations of makers and
artists. Updates the rail button (title/aria-label/chip) and the dialog
heading. Internal ids stay friendsBtn/friendsTitle.

* feat: polish "We Recommend" and add Thomann + Analog4Lyfe

- Stack the rail label onto two centered lines ("We" / "Recommend") so it
  no longer clips the 40px chip, and swap the TV icon for a heart (both the
  rail button and the dialog header).
- Add a monogram avatar fallback: tiles with no image (or a broken image)
  render an on-brand circular initial instead of a broken-image icon.
- Add two recommendations: Thomann (@thomann.music) and Analog4Lyfe
  (@analog4lyfe), in the dialog grid and the README table. Their images
  (static/img/friends/{thomann,analog4lyfe}.jpg) can be dropped in later;
  until then they show the monogram fallback.

---------

Co-authored-by: Thales <>
2026-06-26 12:15:46 +01:00
Tha.Les da93c5ee44 feat: export as MP4 (karaoke video) for MP4 uploads and YouTube (#226)
* feat: export as MP4 (karaoke video) for MP4 uploads and YouTube (#219)

Add an MP4 export that muxes the current mixer state (e.g. vocals muted)
with the source video, producing a karaoke-style video.

Backend:
- Preserve a silent video.mp4 from .mp4 uploads (stream-copy, no re-encode).
- YouTube jobs do a best-effort video-only download (H.264/avc1, <=720p)
  to video.mp4, decoupled from the audio source so failures degrade to
  audio-only. New STEMDECK_VIDEO_MAX_HEIGHT config.
- GET /api/jobs/{id}/video.mp4 streams a fragmented MP4: the amix audio
  graph encoded as AAC, video stream-copied.
- has_video flag on Job, surfaced in state and persisted to metadata.

Frontend:
- MP4 added as a fourth export format (WAV/MP3/FLAC/MP4), shown only for
  jobs with a preserved video track. In MP4 mode, Export Mix produces the
  karaoke video and the audio-only Stems/Region rows are hidden.

SoundCloud and plain audio uploads are audio-only (no MP4 option).

* feat: bundle FFmpeg on Linux via first-launch download

Linux no longer requires `sudo apt install ffmpeg`. The desktop shell now
downloads a static FFmpeg build into the user data dir on first launch
(like Windows/macOS), falling back to a system ffmpeg on PATH when present.
This also fixes Demucs failing to decode compressed sources, since the
download lands in data_dir/ffmpeg which config.json already adds to PATH.

- ensure_ffmpeg: prefer a system ffmpeg, else download_linux_ffmpeg.
- download_linux_ffmpeg: fetch the .tar.xz, extract with system tar,
  copy ffmpeg + ffprobe into data_dir/ffmpeg. STEMDECK_FFMPEG_URL overrides.
- Widen download_file and make_executable from macos to unix so Linux
  reuses them.
- Not bundled in the tarball, so we don't redistribute FFmpeg.
- Update Linux README/notices/packaging comment to drop the ffmpeg apt step.

* style: apply ruff format to MP4 export code

---------

Co-authored-by: Thales <>
2026-06-25 22:46:23 +01:00
Tha.Les 3995e9236f fix: remove orphaned nvidia-* CUDA packages from Linux bundle (#225)
Both Linux tarballs were 2.5 GB (over GitHub's 2 GiB asset limit) even
with CPU torch. Root cause: 'uv pip install <project>' pulls the default
Linux torch, which is the CUDA build, dragging in nvidia-* runtime
packages (cuDNN/cuBLAS/NCCL/...) and triton (~2.5 GB). The CPU torch swap
uses --force-reinstall --no-deps, so torch becomes CPU but those CUDA
packages stay installed and orphaned, bloating the tarball.

Uninstall the nvidia-* packages and triton after the swap. CPU torch does
not use them and the NVIDIA variant re-downloads CUDA at first run.

Co-authored-by: Thales <>
v0.7.0-alpha.17
2026-06-24 20:47:01 +01:00
Tha.Les ab832e3342 fix(ci): skip apt when deps present; the runner has no passwordless sudo (#224)
Root cause of the Linux apt hang: the wsl2 self-hosted runner does not
have passwordless sudo, so 'sudo apt-get' blocks forever at the password
prompt. The previous 'sudo timeout ... apt-get' guard did nothing because
sudo prompts BEFORE the inner timeout can start.

Since the build deps are already installed on the persistent runner, check
each package with dpkg (no sudo) and skip apt entirely when all are present.
Only touch apt if something is genuinely missing, using 'sudo -n' so it
fails fast with a clear message instead of hanging at a prompt.

Co-authored-by: Thales <>
2026-06-24 20:29:55 +01:00
Tha.Les 8210e7f22d fix(ci): make Linux apt step best-effort and hard-capped (#223)
The Linux release job hung for 30+ min on 'install build dependencies':
apt-get stalled on the persistent wsl2 runner (likely a dpkg lock held by
unattended-upgrades). The packages are already installed on that runner
from prior runs, so the install itself is a no-op -- the hang is in
apt-get update / lock acquisition.

Bound the apt commands with  and DPkg::Lock::Timeout so a stuck
lock cannot hang the release, set DEBIAN_FRONTEND=noninteractive to avoid
debconf prompts, treat apt as best-effort, then verify webkit2gtk-4.1 is
actually present (fail loudly only if genuinely missing).

Co-authored-by: Thales <>
2026-06-24 20:06:20 +01:00
Tha.Les 421f2b344c fix: ship CPU torch in Linux NVIDIA variant; download CUDA at first run (#222)
The Linux NVIDIA tarball baked the full CUDA torch wheel, producing an
asset >2 GiB that GitHub release uploads reject (size must be < 2147483648).

On Linux the default PyPI torch wheel bundles the CUDA runtime (~2.5 GB),
unlike Windows where the default wheel is CPU-only. The Windows NVIDIA
package therefore never baked CUDA -- it ships CPU torch and downloads the
CUDA wheel at first run via the desktop shell (install_cuda_torch, which is
cfg(not(macos)) and already covers Linux). Mirror that on Linux: bake the
small CPU torch in both variants; the NVIDIA variant differs only by
omitting the cpu-only marker, so the shell detects the GPU and downloads
CUDA on first launch. Keeps both tarballs well under the 2 GiB limit.

Co-authored-by: Thales <>
2026-06-24 19:17:37 +01:00
Tha.Les 44336a55d5 fix(ci): source release tag from github.ref_name on all self-hosted runners (#221)
* fix(ci): source release tag from github.ref_name on Windows

The Windows release job failed at 'write version files' with
'GITHUB_REF_NAME is not set' on the org-level self-hosted win runner,
while the Linux runner saw the variable fine. $env:GITHUB_REF_NAME is
only injected by Actions runner >= 2.290, so an older self-hosted runner
leaves it empty.

Source the tag from the github.ref_name context (evaluated by Actions
before the step runs) via a job-level REF_NAME env var instead, so the
build no longer depends on the runner version. Replaces all three
$env:GITHUB_REF_NAME usages (version step + both build steps).

* fix(ci): source release tag from github.ref_name on Linux and macOS too

The Linux release job failed at 'write version files' with the same
empty-GITHUB_REF_NAME cause as Windows: the wsl2 self-hosted runner is
also older than 2.290. macOS uses the same pattern and the same class of
runner, so fix all three release workflows consistently to read
github.ref_name from context via a REF_NAME env var.

---------

Co-authored-by: Thales <>
2026-06-24 18:54:25 +01:00
Tha.Les 8131900d0a feat: Linux portable builds (CPU + NVIDIA) and release workflow (#220)
* feat: add CPU-only Linux portable build and release workflow

Adds a Linux .tar.gz portable package mirroring the existing Windows/macOS
build paths. Bundles a python-build-standalone runtime (CPU torch + demucs)
plus the Tauri binary so users extract and run ./StemDeck.

- scripts/linux/make-portable.sh: stages PBS Python, force-installs CPU-only
  torch, builds the Tauri binary, and produces StemDeck-Linux-x64.tar.gz with
  the backend/app + python/ layout find_repo_root resolves at runtime.
- .github/workflows/linux-release.yml: builds on hosted ubuntu-latest on
  release publish; installs Tauri v2 apt deps + uv, ClamAV-scans, uploads.
- packaging/linux/{README-LINUX,THIRD_PARTY_NOTICES}.txt: extract-and-run
  instructions noting ffmpeg + WebKitGTK are system (apt) prerequisites.

FFmpeg is not bundled: the Linux shell expects ffmpeg on PATH. NVIDIA/CUDA
and AppImage variants are intentionally deferred to later phases.

* fix: don't set PYTHONHOME on Linux (breaks PBS stdlib resolution)

The Linux backend failed to start with 'ModuleNotFoundError: No module
named encodings'. PYTHONHOME was being set to python/bin instead of the
prefix python/, so CPython looked for its stdlib under python/bin/lib and
could not boot.

Linux bundles python-build-standalone exactly like macOS, which detects
its own prefix by walking up from bin/ and must NOT have PYTHONHOME set.
The two PYTHONHOME sites were gated #[cfg(not(target_os = "macos"))],
wrongly including Linux alongside Windows. Only Windows -- whose portable
venv keeps the stdlib under base/Lib -- needs PYTHONHOME, so gate both
sites (start_backend and python_stdlib_ok) to #[cfg(windows)].

This also fixes the latent inconsistency where probe_runtime reported
Python ready (python_stdlib_ok set PYTHONHOME=python, the correct prefix)
while start_backend set PYTHONHOME=python/bin and failed.

* feat: add NVIDIA/CUDA Linux portable variant

Adds a second Linux package, StemDeck-Linux-x64.NVIDIA.tar.gz, with
CUDA-enabled torch baked in (mirrors the Windows NVIDIA variant).

- make-portable.sh: CPU_ONLY toggle (default 1). CPU_ONLY=0 keeps the
  project's default torch wheel, which on Linux x86_64 is the CUDA build,
  and omits the cpu-only marker so the desktop shell detects the GPU and
  uses CUDA at runtime. No app-side changes needed -- the CUDA detection/
  install path in main.rs is already cfg(not(macos)) and covers Linux.
- linux-release.yml: builds both variants in one job. CPU first (full Tauri
  build), then NVIDIA with SKIP_TAURI_BUILD=1 reusing the same binary. Adds
  a free-disk-space step (CUDA bundle is several GB) and drops each
  uncompressed stage after taring to stay within the hosted runner's disk.
- README-LINUX.txt: documents both variants and the NVIDIA driver
  prerequisite (nvidia-smi must work; CUDA runtime is bundled, no toolkit
  install needed; falls back to CPU when no GPU).

* ci: run Linux release on self-hosted linux/x64 runner

Targets the org's self-hosted wsl2 runner ([self-hosted, linux, x64])
instead of hosted ubuntu-latest, matching the Windows/macOS release
jobs. Drops the free-disk-space step: it was a hosted-runner workaround
and would needlessly rm system directories on a persistent self-hosted
box (WSL2's virtual disk has ample room for the CUDA bundle).

* ci: add workflow_dispatch test build for Linux release

Lets you run the full two-variant build + ClamAV scan on the self-hosted
runner without publishing a release, to validate the runner toolchain and
the CUDA build. Resolves the version from a manual input (default 0.0.0,
must be valid PEP 440) instead of the branch ref, and skips the upload
step on non-release events.

---------

Co-authored-by: Thales <>
2026-06-24 18:07:34 +01:00
Tha.Les 7be60c47ca fix: support RTX 50-series (Blackwell sm_120) CUDA via cu128 wheels (#217) (#218)
* fix: support RTX 50-series (Blackwell sm_120) CUDA via cu128 wheels

The stock torch 2.6 cu12x wheels have no sm_120 kernels, so Blackwell
GPUs (e.g. RTX 5060 Ti) pass torch.cuda.is_available() but crash mid-
extraction with "no kernel image is available for execution on the
device" (#217).

Targeted carve-out so existing users are untouched:
- detect_nvidia_gpu now also reads the GPU compute capability
- wheel_tag routes sm_100/sm_120 (cap major >= 10) to cu128, everything
  else keeps the existing cu124/cu121/cu118 heuristic
- install_cuda_torch installs torch 2.7.1+cu128 for the cu128 tag and
  stays on 2.6.0 for all other tags
- verify_cuda_torch now forces a real kernel launch instead of trusting
  is_available(), so an incompatible wheel falls back to CPU cleanly
  instead of crashing during a job

Closes #217

* fix(deps): bump msgpack 1.1.2 -> 1.2.1 (GHSA-6v7p-g79w-8964)

Trivy flags msgpack 1.1.2 with a HIGH advisory (out-of-bounds read /
crash on Unpacker reuse). It's a transitive dep via the torch/demucs
stack; bump to the fixed 1.2.1 to clear the trivy fs scan.

---------

Co-authored-by: Thales <>
v0.7.0-alpha.16
2026-06-23 22:28:56 +01:00
Tha.Les 66425fe108 fix: use powershell shell on Windows runner (pwsh not available) (#212) v0.7.0-alpha.15 2026-06-18 10:31:59 +01:00
Tha.Les 17ea467d61 docs: replace Woodpecker CI badge with GitHub Actions (#211) 2026-06-18 09:23:30 +01:00
Tha.Les e817fa7839 feat: add MP4 and M4A upload support, raise limit to 400 MB (#210)
Closes #209
2026-06-18 09:23:27 +01:00
Tha.Les d6280a3efb ci: trigger release workflows only on release published (#208) 2026-06-18 08:58:50 +01:00
dependabot[bot] bca5a8961b chore(deps): bump actions/checkout from 4 to 6 (#206)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 08:45:06 +01:00
Tha.Les a39ef48483 chore: remove Woodpecker CI configuration (#207) 2026-06-18 08:43:04 +01:00
Tha.Les 3baaa0fa07 ci: migrate from Woodpecker to GitHub Actions (#205)
* ci: add GitHub Actions CI workflow (replaces Woodpecker ci.yml)

Parallel jobs: lint, test, js-syntax, sast-bandit, deps-audit, trivy.
Trivy now uses aquasecurity/trivy-action instead of the container image.

* fix: upgrade yt-dlp, starlette, python-multipart; ignore new torch CVEs

- yt-dlp 2026.3.17 -> 2026.6.9 (fixes CVE-2026-50023, CVE-2026-50574, GHSA-69qj-pvh9-c5wg)
- starlette 1.0.0 -> 1.3.1 (fixes CVE-2026-48818, CVE-2026-54283)
- python-multipart 0.0.27 -> 0.0.32 (fixes CVE-2026-53539)
- Add CVE-2025-2148/2149/2998/2999/3000/3001 to deps-audit ignore list:
  torch is pinned below 2.7 due to torchaudio/demucs compat; these CVEs
  are in ops StemDeck does not invoke.

* ci: add macOS and Windows release workflows + Dependabot config

- macos-release.yml: builds arm64 and x64 DMGs on self-hosted macOS runner,
  inspects artifacts, uploads to GitHub release via softprops/action-gh-release
- windows-release.yml: builds NVIDIA and CPU portable ZIPs on self-hosted
  Windows runner, scans with ClamAV, uploads to GitHub release
- dependabot.yml: weekly action SHA bumps for all workflows
- Both release workflows: permissions locked to read-only at workflow level,
  contents:write only on the job; concurrency guard; 120/90 min timeouts;
  workspace cleanup; all actions pinned to SHA

* ci: use osx runner label for macOS release workflow
2026-06-17 21:28:40 +01:00
Tha.Les 76d6c351bb Update README.md
not supporters but name only
2026-06-11 12:33:21 +01:00
Thales Pereira 86b1e87a24 docs: tighten intro copy and surface We Recommend in the header v0.7.0-alpha.14 2026-06-09 13:38:22 +01:00
Tha.Les 907ae7f956 feat: expand Supporters (Joao, Kris), Instagram links/avatars, and README We Recommend (#202)
Adds Joao Gaspar and Kris Luthier (with bundled Instagram profile images) to the Supporters dialog; points Dlima Guitars at Instagram. Tiles gain optional role lines, round avatars for IG photos, a small Instagram glyph on IG-linked tiles, and a masonry/tilted layout. Warmer dialog tagline. README gains a We Recommend section with a no-funding disclaimer, and the old donation line now points to it.
2026-06-09 13:33:06 +01:00
Tha.Les ed09e802c5 docs: add We Recommend section listing supporters (#__) 2026-06-09 12:27:45 +01:00
Tha.Les 86f690da62 feat: add Joao Gaspar and Kris Luthier to Supporters, masonry tile layout (#200)
Adds Joao Gaspar and Kris Luthier (with locally bundled IG profile images) to the Supporters dialog. Tiles gain an optional role line, render gracefully without a logo, and lay out as independent masonry columns with a slight per-tile tilt (frames-on-a-wall look) that straightens on hover.
2026-06-09 11:55:23 +01:00
Tha.Les 6761831abc fix: point runtime pack URL at stemdeckapp, not old thcp repo (#199)
CI never sets RELEASE_BASE_URL, so every macOS release manifest baked the old thcp/stemdeck download URL. It only worked via GitHub's transfer redirect, which is outside our control and would 404 if a repo named stemdeck is ever recreated under thcp. Point the default at the repo we own. Future releases only.
v0.7.0-alpha.13
2026-06-09 10:00:48 +01:00
Tha.Les 091ba3fd1b docs: Code of Conduct, Contributing guide, and issue templates (#198)
Adds the community health files: Contributor Covenant 2.1 Code of Conduct (private reporting via Security tab), a Contributing guide (layout, dev setup, CI gates, commit/PR conventions), and .github/ISSUE_TEMPLATE YAML forms (bug, feature) with config.yml linking Discussions/Discord/security.
2026-06-07 19:00:40 +01:00
Tha.Les 64dc5f7a6d feat: Supporters dialog behind a TV rail icon (#197)
A TV icon in the sidebar rail (between Settings and Help) opens an About-style 'Supporters' dialog with partner tiles (Dlima Guitars, Lisbon Guitar Works), clickable to their sites. Logos bundled; rail widened 56->66px so the label fits. Verified headless.
v0.7.0-alpha.12
2026-06-06 00:51:23 +01:00
Tha.Les a74ae01404 fix: overview waveforms - align to lanes and only show extracted stems (#196)
Subset extractions misaligned the waveform overlay and (after the first attempt) showed non-extracted stems. Render one row per mixer lane for alignment, and draw bars only for the extracted/selected stems (plus original); other lanes stay empty. Verified headless.
v0.7.0-alpha.11
2026-06-05 23:34:06 +01:00
Tha.Les 23a3a3d930 fix: refresh runtime pack on app upgrade + stop false update banner (#195)
On a DMG upgrade the old runtime was kept because setup.js checked the runtime version after the 'runtime ready' early-return (dead since #779a2e6). Move the check before the early-return and treat unknown installed versions as a mismatch, so upgrades re-download the new runtime (backend+frontend). Also compare update-banner versions canonically (PEP440 vs tag) so a current app is not nagged.
v0.7.0-alpha.10
2026-06-05 18:23:34 +01:00
Tha.Les 0a67593ad4 feat: add FLAC support (import and export) (#flac) (#194) v0.7.0-alpha.9 2026-06-05 15:36:49 +01:00
Tha.Les 4ccb70b31e docs: add a real SECURITY.md (#193)
Replaces GitHub's placeholder template: private vulnerability reporting (now enabled), latest-alpha-only support, realistic response expectations, and a local-first/single-user threat model.
2026-06-05 13:42:53 +01:00
Tha.Les 471cafdb17 fix: verify macOS FFmpeg download with a pinned SHA256 (#172) (#192)
macOS setup ran the downloaded FFmpeg/ffprobe with no integrity check. evermeet publishes no .sha256 companion, so pin a specific versioned build + its SHA256 and verify before extract/exec; mismatch deletes the file. Override URLs may supply STEMDECK_FFMPEG_SHA256/STEMDECK_FFPROBE_SHA256. Adds 4 Rust tests.
v0.7.0-alpha.8
2026-06-05 13:26:55 +01:00
Tha.Les 7b566e1c2e feat: Export Mix reflects the mixer (volume, mute, solo) (#183) (#191)
Export Mix now renders on demand from the current mixer state - per-stem volume, mute, and solo - via a new /jobs/{id}/mixdown.{ext} ffmpeg endpoint. Master fader is intentionally excluded; Export All Stems stays raw. Adds 13 backend tests; verified end-to-end (gain 0.5 -> half RMS, amix sums faithfully).
v0.7.0-alpha.7
2026-06-05 12:49:55 +01:00
Tha.Les 60f998160e fix: engine overview waveform - align to ruler and restore bar style (#189) (#190)
Two regressions from #187 (SVG overview became the visible waveform in engine mode): a constant 48px left gap vs the ruler/playhead, and the bar art style replaced by a filled envelope. Zero the legacy 48px offset in the DAW layout and render the overview as WaveSurfer-style bars (3px bar / 2px gap, baseline for silence). Verified headless in WebKit.
v0.7.0-alpha.6
2026-06-05 06:40:05 +01:00