- Downloads a small separation model (UVR MDX-Net Voc FT, ~64 MB, from
- the Ultimate Vocal Remover project) that lifts the singing away from
- the band before words are timed — noticeably steadier timing on dense
- mixes. Same rules as above: runs on this computer, in the background,
- never against the playing track. Timed songs re-prepare the next time
- you open their lyrics.
+
+
+
+
Account Your Soulseek login and where the password lives.
+
+
+
+
Soulseek account {#if settings.connected || online}online {:else if settings.username}offline {/if}
+
{settings.username ? `Signed in as ${settings.username}.` : "Not signed in."} Your password is stored in the Windows Credential Manager, not in a plain file.
+
+
+ {#if app.connection !== "online"}
+ (app.showLogin = true)}>Connect
+ {/if}
+ {#if settings.has_password}
+ Forget credentials
+ {/if}
- {#if sepModel?.present}
-
Downloaded
- {:else}
-
- {sepBusy ? `Downloading… ${sepPct}%` : "Download (64 MB)"}
-
+
+
+
+
+
+
Networks The two networks this app speaks, and whether they are tied together.
+
+
+
+
Soulseek {#if online}online {:else}offline {/if}{#if ownLinkCurrent}linked {/if}
+
+ {#if me}Signed in as {me} · {fmtCount(network.shareFiles)} files shared.{:else}Not signed in.{/if}
+ {#if !key}
+ Create a SoulStream identity on Community → You to link this account to it.
+ {:else if !me}
+ Sign in to Soulseek to link this account to your identity.
+ {:else if ownLinkCurrent}
+ Linked to your SoulStream identity — proof verified {new Date(ownLink.verified_at * 1000).toLocaleString()} through a message to yourself. Other SoulStream users who ask get a signed answer.
+ {:else}
+ Not linked to your identity. Linking sends one message to yourself over Soulseek and verifies the signed answer — nothing is published.
+ {/if}
+
+
+
+ {#if key && me}
+ {#if ownLinkCurrent}
+ unlinkSelf()} disabled={!!community.busy.linkself}>Unlink
+ {:else}
+ linkSelf()} disabled={!online || !nodeUp || !!community.busy.linkself}> {community.busy.linkself ? "Linking…" : "Link to my identity"}
+ {/if}
+ {/if}
+
+
+
+
+
Freenet experimental
+
+ {#if freenetOn}
+ Local node on 127.0.0.1:{community.freenet?.ws_port ?? 7509} · {status.text}
+ detectNode()} disabled={!!community.busy.detect}>{community.busy.detect ? "checking…" : "check again"}
+ {:else}
+ Off. Carries your identity, profile, playlists, friend links, Spaces and holdings. Turning it on publishes nothing; turning it off is instant and touches nothing else in the app.
+ {/if}
+
+ {#if freenetOn && statusKind(community.freenet?.status) === "node-absent"}
+
+ {#if community.nodeInfo?.binary}
+ startNode()} disabled={!!community.busy.node}> {community.busy.node ? "Starting…" : "Start node"}
+ runs the installed peer{#if community.nodeInfo.version} ({community.nodeInfo.version}){/if} in the background · telemetry pinned off · data in the app folder
+ {:else}
+ No Freenet peer found on this machine — install it (cargo install freenet) and check again.
+ {/if}
+
+ {:else if freenetOn && community.nodeInfo?.started_by_app}
+
+ stopNode()} disabled={!!community.busy.node}>{community.busy.node ? "Stopping…" : "Stop node"}
+ started by SoulStream · keeps running when the app closes; this button stops it
+
+ {/if}
+
+ Before you rely on it — seven things that are true
+
+ A desktop background peer must run; it uses network, disk, memory and CPU.
+ The network is alpha. Things you publish are replicated and cannot be guaranteed erased.
+ Freenet is not an anonymity service.
+ The node's own diagnostics are a node setting SoulStream pins off; verify after upgrades.
+ Nothing is published when you enable this. Publishing is always a button.
+ Nothing here ever says "online" — a successful fetch is not presence.
+ Phones and TVs cannot run the same peer today.
+
+
+
+
{@render sw(freenetOn, ["on", "off"], (v) => setFreenetEnabled(v), !!community.busy.freenet)}
+
+ {#if freenetOn}
+
+
+
Load remote profile images
+
Other people's avatars, fetched from the network — verified, resized, cached by hash. Off shows their identicon only.
+
+
{@render sw(community.freenet?.load_remote_images ?? true, ["on", "off"], (v) => setLoadRemoteImages(v), !!community.busy.images)}
+
+ {#if key}
+
+
+
Encrypted inbox {#if community.inbox?.published} key {community.inbox.box_key_id} {/if}
+
{#if community.inbox?.published}Friend requests and shares arrive sealed to a key kept on this device{#if community.inbox.last_check} · checked {fmtWhen(community.inbox.last_check * 1000)}{/if}.{:else}Lets other SoulStream users send you friend requests and shares sealed, without Soulseek in between.{/if}
+
+
+ {#if community.inbox?.published}
+ checkInbox()} disabled={!nodeUp || !!community.busy.inboxcheck}>{community.busy.inboxcheck ? "Checking…" : "Check now"}
+ {:else}
+ publishInbox()} disabled={!nodeUp || !!community.busy.inbox}> {community.busy.inbox ? "Publishing…" : "Publish an inbox"}
+ {/if}
+
+
+ {/if}
+ {/if}
+ {#if community.error}{/if}
+
+
+
+
+
+
Appearance How the window is drawn.
+
+
+
+
Dark glass
+
Lets whatever is behind the window glow through the app's dark surfaces with a live blur (Windows acrylic). Cards, rows and highlights thin out with it; menus and dialogs stay solid so nothing gets hard to read.
+
+
{@render sw(!!settings.glass_mode, ["dark glass", "solid"], (v) => setGlassMode(v), saving)}
+
+
+
+
+
+
+
Playback Output and loudness.
+
+
+
+
Audio output
+
System default follows Windows when headphones or speakers change.
+
+
+
+
+
+
+
+
Normalize volume
+
Plays every track at roughly the same loudness (−14 LUFS, like Spotify), so quiet and loud masters stop needing the volume knob. A track is measured while it first streams and matched exactly from then on; until that finishes SoulStream will turn it down but never up, so a quiet intro can't make the chorus too loud.
+
+
{@render sw(!!settings.volume_normalization, ["on", "off"], (v) => setVolumeNormalization(v), saving)}
+
+
+
+
+
+
+
+
Lyrics Line sync is always on; word fill is the experiment.
+
+
+
+
Word-by-word fill experimental
+
Fills each word as it is sung, karaoke-style. Still experimental: timing can drift on dense or echo-heavy songs. Off keeps the reliable line-by-line sync everywhere and pauses the background word analysis.
+
+
{@render sw(!!settings.word_timing, ["on", "off"], (v) => setWordTiming(v), saving)}
+
+ {#if settings.word_timing}
+
+
+
Precise word timing
+
Downloads an alignment model (MMS, ~1.26 GB, from Hugging Face) that listens to each lyric line and times every word against the actual singing, in most languages. Runs entirely on this computer, in the background, never against the playing track. Without it, word timing stays a lighter estimate. Already-timed songs re-prepare automatically the next time you open their lyrics.
+ {#if modelError}
Download failed: {modelError}
{/if}
+
+
+ {#if lyricModel?.present}
+ Downloaded
+ {:else}
+ {modelBusy ? `Downloading… ${modelPct}%` : "Download (1.26 GB)"}
+ {/if}
+
+
+ {#if lyricModel?.present}
+
+
+
Vocal isolation
+
Downloads a small separation model (UVR MDX-Net Voc FT, ~64 MB, from the Ultimate Vocal Remover project) that lifts the singing away from the band before words are timed — noticeably steadier timing on dense mixes. Same rules as above: runs on this computer, in the background, never against the playing track.
+ {#if sepError}
Download failed: {sepError}
{/if}
+
+
+ {#if sepModel?.present}
+ Downloaded
+ {:else}
+ {sepBusy ? `Downloading… ${sepPct}%` : "Download (64 MB)"}
+ {/if}
+
+
+ {/if}
{/if}
- {#if sepError}
- Download failed: {sepError}
- {/if}
- {/if}
- {/if}
-
+
-
- Streaming
-
-
-
Keep streamed tracks
-
Streamed songs stay in a private cache for instant replay — not in your library. Choose Forever to add everything you stream to the library instead.
-
-
setStreamRetention(e.currentTarget.value)}
- >
- 1 day
- 3 days
- 1 week
- 2 weeks
- 1 month (recommended)
- 3 months
- Forever — save to library
-
-
-
-
-
Cache size limit
-
Oldest streamed tracks are removed first once the cache passes this size. Saved and downloaded music is never touched.
-
-
setStreamCacheMax(e.currentTarget.value)}
- >
- 512 MB
- 1 GB
- 2 GB (recommended)
- 5 GB
- 10 GB
- 20 GB
- Unlimited
-
-
- Retention counts from the last time a track was played. Tracks you save stay in your library regardless.
-
-
-
- Library & downloads
-
-
-
Library folder
-
{settings.download_dir}
-
-
Change…
-
- This is your downloaded-music library and is always shared. Folder changes apply after reconnecting.
-
-
-
Album folder format
-
Use {"{artist}"}, {"{album}"}, {"{date}"}, or {"{year}"}; "\" starts a subfolder. Full dates display as DD-MM-YYYY.
-
-
e.key === "Enter" && e.currentTarget.blur()}
- onchange={(e) => setLibraryLayout(e.currentTarget.value)}
- />
-
- New downloads are organized into this format; existing files are not moved.
-
-
-
- Sharing
-
- Sharing keeps you a good citizen — leechers get banned. Your download
- folder is always shared ({settings.share_files} files indexed).
- Add extra folders below.
-
- {#if settings.shared_folders.length > 0}
-
- {#each settings.shared_folders as folder (folder)}
-
-
{folder}
-
removeSharedFolder(folder)}
- >
+
+
+
Streaming What happens to a track after you streamed it.
+
+
+
+
Keep streamed tracks
+
Streamed songs stay in a private cache for instant replay — not in your library. Forever adds everything you stream to the library instead.
+
+
+
+
- {/each}
-
- {/if}
- Add folder…
-
-
-
- Album artwork
- Covers are resolved from Deezer, Apple, and the Cover Art Archive; the artwork shown in the catalog wins, at the highest resolution any source serves. One canonical image is used for the library, the embedded tags, and the album folder.
-
-
-
Art inside downloaded files
-
Replace whatever picture the uploader embedded with the canonical cover, or keep theirs when present.
-
-
setReplaceEmbeddedArt(e.currentTarget.value)}
- >
- Replace with canonical (recommended)
- Keep uploader's art
-
-
-
-
-
Embedded cover size
-
Longest edge of the copy written into each audio file. The full-resolution original is always kept in the album folder and art cache.
-
-
setEmbedArtMaxEdge(e.currentTarget.value)}
- >
- Original (largest files)
- 1500 px (recommended)
- 1000 px
-
-
-
-
-
Album folder cover
-
Write the canonical cover as cover.jpg and folder.jpg so Explorer and other players show the same artwork.
-
-
setWriteFolderCover(e.currentTarget.value)}
- >
- Write cover.jpg + folder.jpg
- Leave folders untouched
-
-
-
-
-
Re-fetch all album covers
-
Upgrade every library album to the canonical high-resolution cover — cache, embedded tags, and folder files. Roughly one album per second.
-
-
- {refreshingAlbums ? "Upgrading…" : "Re-fetch covers"}
-
-
- {#if albumRefreshResult}{albumRefreshResult}
{/if}
-
-
-
- Artist artwork
-
-
-
Hero renderer
-
Live GPU creates one native-4K master, then mip-filters it cleanly for every window size.
-
-
setArtistHeroUpscaler(e.currentTarget.value)}
- >
- Live GPU 4K (recommended)
- Original baked FSR 1
- Baked Lanczos3
-
-
- Live GPU applies crop ? EASU ? RCAS ? color/fade/grain once at 3840?2160. Resizing only re-samples that fixed master, preserving grain scale and avoiding baked-file latency.
-
-
-
Refresh artist portraits
-
Fetch portraits for artists already in your library.
-
-
- {refreshingArtists ? "Refreshing…" : "Refresh artwork"}
-
-
- {#if artistRefreshResult}{artistRefreshResult}
{/if}
-
-
-
- Source quality
-
- Independent preferences for streaming and downloading. The ceiling and floor
- bound which peer files qualify; priority decides whether the resolver picks
- the best format or the fastest transfer among them.
-
- {#each [
- { key: "stream", label: "Streaming" },
- { key: "download", label: "Downloads" },
- ] as mode (mode.key)}
-
-
-
{mode.label} — maximum
-
Skip files above this ceiling (lossy-only avoids huge lossless transfers).
+
+
+
Cache size limit
+
Oldest streamed tracks are removed first once the cache passes this size. Saved and downloaded music is never touched.
+
+
+
+
+
+
-
setSourcePref(mode.key, "max", e.currentTarget.value)}
- >
- No limit (lossless preferred)
- Lossy only (≤ 320 kbps)
-
-
-
-
-
{mode.label} — minimum
-
Reject files below this floor. Lossy files must report a bitrate to qualify.
-
-
setSourcePref(mode.key, "min", e.currentTarget.value)}
- >
- Any quality
- 192 kbps and up
- 256 kbps and up
- 320 kbps and up
- Lossless only
-
-
-
-
-
{mode.label} — priority
-
- Fastest transfer estimates arrival time from file size ÷ peer speed
- (plus queue wait) and picks whatever lands soonest — a large FLAC from a
- fast peer can win. Only files inside your min/max window compete.
+
+
+
+
+
Library & downloads Where music lands and how it is filed.
+
+
+
+
Library folder
+
{settings.download_dir}
+
Your downloaded-music library, always shared. Folder changes apply after reconnecting.
+
+
Change…
+
+
+
+
Album folder format
+
Use {"{artist}"}, {"{album}"}, {"{date}"} or {"{year}"}; "\" starts a subfolder. Full dates display as DD-MM-YYYY. New downloads are organized into this format; existing files are not moved.
+
+
+ e.key === "Enter" && e.currentTarget.blur()} onchange={(e) => setLibraryLayout(e.currentTarget.value)} />
+
- setSourcePref(mode.key, "priority", e.currentTarget.value)}
- >
- Best quality
- Fastest transfer (est. time)
-
-
- {/each}
-
+
-
- Uploads
-
-
-
Upload slots
-
How many peers can download from you at once.
-
-
- setSlots(settings.upload_slots - 1)}>
-
-
- {settings.upload_slots}
- setSlots(settings.upload_slots + 1)}>
-
-
-
+
+
+
Sharing Sharing keeps you a good citizen — leechers get banned.
+
+
+
+
Shared folders
+
Your library folder is always shared ({fmtCount(settings.share_files)} files indexed). Add extra folders here.
+
+
Add folder…
+
+ {#if settings.shared_folders.length > 0}
+
+ {#each settings.shared_folders as folder (folder)}
+
{folder} removeSharedFolder(folder)}>
+ {/each}
+
+ {/if}
+
+
+
Upload slots
+
How many peers can download from you at once.
+
+
+
+ setSlots(settings.upload_slots - 1)} disabled={saving} aria-label="Fewer slots">
+ {settings.upload_slots}
+ setSlots(settings.upload_slots + 1)} disabled={saving} aria-label="More slots">
+
+
+
+
+
+
+
+
+
Source quality Independent preferences for streaming and downloading.
+
+
+ {#each [{ key: "stream", label: "Streaming" }, { key: "download", label: "Downloads" }] as mode (mode.key)}
+
{mode.label}
+
+
+
Maximum
+
Skip files above this ceiling — lossy-only avoids huge lossless transfers.
+
+
{@render seg([["any", "No limit"], ["lossy_only", "Lossy only"]], sourcePrefsFor(mode.key).max, (v) => setSourcePref(mode.key, "max", v), saving)}
+
+
+
+
Minimum
+
Reject files below this floor. Lossy files must report a bitrate to qualify.
+
+
+
+
+
+
+
+
Priority
+
Only files inside the window above compete.
+
+
{@render seg([["quality", "Best quality"], ["speed", "Fastest transfer"]], sourcePrefsFor(mode.key).priority, (v) => setSourcePref(mode.key, "priority", v), saving)}
+
+ {/each}
+
+
+
+
+
+
Album artwork Covers come from Deezer, Apple and the Cover Art Archive; the catalog's wins, at the highest resolution any source serves.
+
+
+
+
Art inside downloaded files
+
Replace whatever picture the uploader embedded with the canonical cover, or keep theirs when present.
+
+
{@render seg([["replace", "Replace with canonical"], ["keep", "Keep uploader's art"]], settings.replace_embedded_art ? "replace" : "keep", (v) => setReplaceEmbeddedArt(v), saving)}
+
+
+
+
Embedded cover size
+
Longest edge of the copy written into each audio file. The full-resolution original is always kept in the album folder and art cache.
+
+
{@render seg([["0", "Original"], ["1500", "1500 px"], ["1000", "1000 px"]], String(settings.embed_art_max_edge), (v) => setEmbedArtMaxEdge(v), saving)}
+
+
+
+
Album folder cover
+
Write the canonical cover as cover.jpg and folder.jpg so Explorer and other players show the same artwork.
+
+
{@render sw(!!settings.write_folder_cover, ["write", "leave"], (v) => setWriteFolderCover(v ? "write" : "skip"), saving)}
+
+
+
+
Re-fetch all album covers
+
Upgrade every library album to the canonical high-resolution cover — cache, embedded tags and folder files. Roughly one album per second.
+ {#if albumRefreshResult}
{albumRefreshResult}
{/if}
+
+
{refreshingAlbums ? "Upgrading…" : "Re-fetch covers"}
+
+
+
+
+
+
+
Artist artwork Portraits and the hero renderer.
+
+
+
+
Hero renderer
+
Live GPU creates one native-4K master (crop → EASU → RCAS → color/fade/grain once at 3840×2160), then mip-filters it cleanly for every window size, preserving grain scale.
+
+
{@render seg([["live_gpu", "Live GPU 4K"], ["fsr_legacy", "Baked FSR 1"], ["lanczos3", "Baked Lanczos3"]], settings.artist_hero_upscaler ?? "live_gpu", (v) => setArtistHeroUpscaler(v), saving)}
+
+
+
+
Refresh artist portraits
+
Fetch portraits for artists already in your library.
+ {#if artistRefreshResult}
{artistRefreshResult}
{/if}
+
+
{refreshingArtists ? "Refreshing…" : "Refresh artwork"}
+
+
+
-
-
- {#if saving}
-
Saving…
- {:else if savedFlash}
-
✓ Saved
- {:else if saveError}
-
{saveError}
- {/if}
+
{/if}