Skip to content

Dev 2.1 - #124

Open
TheJoeFin wants to merge 25 commits into
mainfrom
dev-2.1
Open

Dev 2.1#124
TheJoeFin wants to merge 25 commits into
mainfrom
dev-2.1

Conversation

@TheJoeFin

Copy link
Copy Markdown
Owner

No description provided.

TheJoeFin and others added 14 commits September 5, 2026 22:54
Groundwork for a generated FM static bed that fills the silence while a
stream buffers. Defaults to off: listeners who already have the app
installed should not suddenly hear noise out of it, which reads as a bug
or a blown speaker rather than as a feature.

Carries a change event alongside the value, following
IsSongChangePopupEnabled, so static that is already playing can be faded
out the moment the setting is switched off rather than finishing its
burst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVqRZ9FUfAyKjuXuoxZvFT
The moment the app is most like a radio - the pause while a stream loads -
was dead silent. This fills it with static synthesised from NAudio
generators, ramping in so it never arrives abruptly.

Three layers: band-limited pink noise with a slow swell for the hiss
(pink, not white, is what reads as radio rather than TV snow), a wandering
tone for the heterodyne whine, and a slow sweep drifting up and back down
behind both. The whine takes a fresh pitch each burst so repeated
buffering does not keep opening on the same note, and it sits above the
band where the noise holds most of its energy - a tone in the middle of
the noise is masked almost perfectly however loud it is.

The sweep is hand-rolled rather than SignalGeneratorType.Sweep, which runs
one way and resets its phase to restart; at an audible level that reset is
a click every pass. Sweeping back down keeps phase continuous forever.

Two timings are deliberate. The fade-out matches RadioPlayerService's
350ms FadeInDuration, so the static recedes over exactly the window the
stream comes up in, giving a cross-fade rather than a gap. And the render
buffer is requested at 50ms instead of NAudio's 200ms default: at the
default the first sample landed on the speakers around a fifth of a second
after buffering began, long after the outgoing station had finished
fading, so the two never overlapped. Measured, the static is now audible
within ~55-65ms and at full level by ~210ms, against a 220ms station fade.

The render device lingers briefly after going quiet instead of being torn
down. Buffering comes in clusters - a flaky stream, or hopping stations -
and a player already running picks up a fade within one buffer rather than
re-activating WASAPI first.

Everything is best-effort: no output device, or one held in exclusive
mode, degrades to silence rather than throwing. This is decoration on top
of playback and is never worth failing over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVqRZ9FUfAyKjuXuoxZvFT
The watchdog detects a dead stream by watching system loopback RMS and
recovering after a few seconds of silence. Radio static is our own noise
on that same output, and loopback capture cannot tell it apart from stream
audio - measured at roughly thirty times the silence threshold. A stream
that died and sat buffering forever would have looked alive for as long as
the static played, quietly suppressing the recovery that is supposed to
rescue it.

The monitor now takes a predicate naming the stretches where the output is
app-generated and scores them as silence. Before the static existed the
output really was silent during buffering, so this keeps the watchdog
seeing exactly what it saw before. PlaybackErrorService reads the same
level signal to decide whether an error still matches audible output, and
is fixed by the same change.

The spectrum visualiser also captures loopback and will react to the
static. That one is left alone - a visualiser moving to the static while a
station loads is the feature working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVqRZ9FUfAyKjuXuoxZvFT
Puts the toggle next to the buffer level, which is the setting it relates
to, following the song change popup card's layout of toggle, state text
and a button.

The Test button plays a short burst regardless of whether the toggle is
on, since it is a preview of what turning it on would sound like, and
waiting for a slow stream is a poor way to audition a sound. It is
disabled for the duration so a second click cannot stack two bursts.

English strings only; the other eight locales fall back to en-US until
translators pick them up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVqRZ9FUfAyKjuXuoxZvFT
RadioStaticProfile holds the tunable constants and the random walks behind
the static, and is deliberately free of NAudio and WinRT types so it can
be linked into the test project the way the playback policies are.

What is worth asserting is that the gain tracks the player's volume and
stays clamped to its 0-2 range, and that the random walks cannot leave the
bands they are meant to wander inside - a swell that escaped its bounds
would click, and a carrier that did would whistle. Also checks that
consecutive bursts really do start the whine on different pitches, since
that is the point of randomising it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVqRZ9FUfAyKjuXuoxZvFT
Introduces AudioSourceKind (Radio, WhiteNoise, Files) so a station's
playback path can be switched on explicitly instead of inferred, plus
WhiteNoiseColor for the noise spectrum a white-noise station plays.
RadioStation gains SourceKind, WhiteNoiseColor, and a WhiteNoiseStreamUrl
placeholder for the (still required) StreamUrl on a station that has
nothing to dial.

Also keeps the format's backwards-compatibility contract: a write-only
IsWhiteNoise property migrates a pre-SourceKind file's legacy boolean
field, and an IJsonOnDeserialized hook self-heals SourceKind from the
placeholder StreamUrl if it was ever saved wrong - the one signal that
survives no matter which shape SourceKind itself was written in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VN8ALA8VhB8SGX7B2iUkXy
Adds WhiteNoisePlaybackEngine, a small WASAPI-based white/pink noise
generator built the same way as the existing buffering-static player but
kept deliberately separate from it: static tells the silence monitor to
discount its own output so a dead stream hiding behind it still recovers,
which is exactly wrong for a source whose audio *is* the content.

RadioPlayerService tracks which AudioSourceKind is currently active and
switches on it in Play/Pause/IsPlaying/IsBuffering, bypassing the
network/buffering pipeline entirely for white noise. The switch happens
inside TransitionToStationAsync at the same point SetStreamUrl already
did - right after the outgoing source is faded out and paused using its
own (still old) kind, not the incoming one, so a transition away from a
playing station is never skipped because IsPlaying started reading the
wrong engine mid-transition.

The stream watchdog now skips its whole recovery ladder for anything
that isn't Radio - there is no network stream to stall or reconnect -
and Play()/PlayWithBufferAsync() degrade to a no-op instead of throwing
when nothing has been initialized yet, matching how Pause() already
handled that case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VN8ALA8VhB8SGX7B2iUkXy
The stream-URL validation that gates selecting, restoring, saving and
initializing a station now only applies to Radio - anything else carries
a placeholder there by design. Station transitions forward SourceKind
and WhiteNoiseColor to RadioPlayerService so it knows what it is about
to switch to.

AddWhiteNoiseViewModel is the counterpart to AddStationViewModel for
this kind of station: a name, a noise color, and a volume, saved as a
RadioStation with SourceKind set to WhiteNoise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VN8ALA8VhB8SGX7B2iUkXy
Right-clicking (or press-and-hold on touch) the existing "Or add station
manually" link now also offers "Add white noise..." - kept off the
permanently visible UI since it's a niche option next to the common
case. It opens a new AddWhiteNoise page (name, noise color, volume),
matching AddStation's layout and navigation pattern, and is where
editing an existing white-noise station goes too rather than the
generic manual-entry window, which has fields (stream URL, homepage,
favicon) that don't apply to it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VN8ALA8VhB8SGX7B2iUkXy
Both dialogs rendered their message via a plain string Content, which
WinUI turns into a TextBlock that can't be selected - so a user hitting
an error had no way to copy it into a bug report. Both now build the
TextBlock explicitly with IsTextSelectionEnabled set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VN8ALA8VhB8SGX7B2iUkXy
NAudio's SignalGenerator only covers white and pink, so
ColoredNoiseSampleProvider implements the rest directly: brown via a
leaky integrator, blue and violet via differencing, and grey via a
shelf/peak EQ approximating the inverse equal-loudness contour. Each
channel keeps independent filter state to preserve stereo image.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HL9SeDkHXU5b2bTFubR59N
Swap the two-way white/pink radio buttons for a ComboBox covering all
six colors, and add a per-color description bound to the selection
instead of the old static caption text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HL9SeDkHXU5b2bTFubR59N
Counts down on a one-second DispatcherQueueTimer and pauses playback
when it elapses. Exposes IsSleepTimerActive, a rounded-up "Xm" display
string, and a 0-1 progress fraction for a countdown UI to bind to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HL9SeDkHXU5b2bTFubR59N
Right-clicking play/pause now offers 15/20/30/45/50 minute presets
under a disabled "Sleep timer" label, kept as flat sibling items
rather than a submenu so picking one takes a single click. While a
timer is running, a countdown ring squeezes in between the play
button and the pop-out link (taking its space from the play button's
stretch column), showing "Xm" and swapping to a cancel icon on hover.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HL9SeDkHXU5b2bTFubR59N
@TheJoeFin TheJoeFin added the enhancement New feature or request label Sep 6, 2026
TheJoeFin and others added 11 commits September 6, 2026 13:58
Scans a folder for audio tracks, plays them through the existing radio
streaming pipeline (Files stations resolve to a real file URI before
reaching the backend), and adds Duration/Seek/PlaybackEnded support to
both playback backends for scrubbing and auto-advance between tracks.
Radio static and metadata publishing are also scoped to skip local
files, which have neither a signal to search for nor stream metadata
to poll.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HL9SeDkHXU5b2bTFubR59N
PlayerViewModel exposes seek/duration/position and next/previous
local-track commands over the service layer, plus a poll timer that
drives a smooth scrub bar only while a local music station is playing.
NowPlayingViewModel surfaces the current folder's track list for the
details page, with music-service search links hidden for local music
since a filename has no guaranteed match on those services.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HL9SeDkHXU5b2bTFubR59N
Adds the pop-out Add/Edit Local Music window (folder picker, name,
volume, artist-folder-of-albums detection) reachable from the search
page's add-station menu and from a Files station's edit action. The
Now Playing details page lists the current folder's tracks, and the
mini/full player adds prev/next track buttons and a scrub bar for
local music.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HL9SeDkHXU5b2bTFubR59N
Added WindowEx_Closed handlers to AddLocalMusicWindow and ManualStationWindow to call ShowFlyout on close. Updated TutorialWindow to use ShowFlyout. Fixed XAML binding formatting in AddLocalMusicWindow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YmA2PGuCdinefSa1mJkYs
Trdo/Services/Audio/RadioStaticService.cs
Trdo/Services/Audio/WhiteNoisePlaybackEngine.cs
Trdo/ViewModels/AddLocalMusicViewModel.cs
Trdo/ViewModels/AddWhiteNoiseViewModel.cs

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YmA2PGuCdinefSa1mJkYs
ShowFlyout now takes an optional TrayIconEventArgs so both the
in-app trigger (window close) and the tray icon's own click handler
can share one code path instead of TryShowFlyout/ShowFlyout duplicating
the pointer-anchor logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YmA2PGuCdinefSa1mJkYs
Position and duration labels now sit on either side of the seek
slider in their own grid columns instead of stacked underneath it,
matching the layout used elsewhere in the player.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YmA2PGuCdinefSa1mJkYs
LocalMusicPlaybackPolicy centralizes the decision of what plays next
when a local track ends (advance, repeat the track, repeat the album,
or stop), backed by a persisted SettingsService.LocalMusicLoopMode
setting and unit tests covering each mode at the boundary track.

RadioPlayerService now guards track-end handling with a completion
gate to prevent re-entrancy, and raises LocalTrackChanged so
PlayerViewModel's local-track state stays in sync when the track
advances on its own rather than via an explicit user action.

Also stop treating a missing internet connection as a reason to abort
local file playback - that check only makes sense for radio streams.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YmA2PGuCdinefSa1mJkYs
Local track rows now read their title from ID3/audio tags via
TagLibSharp, falling back to the filename when a track has no title
tag. The queue list highlights the currently playing track through
ListView selection bound to the new CurrentLocalTrackIndex property
instead of a manually-drawn overlay border, so it also updates when
playback advances automatically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YmA2PGuCdinefSa1mJkYs
Refactored NowPlayingPage to use ListView's SelectionChanged for track selection, removing IsCurrent from LocalTrackDisplayItem. Added LocalTrackListChanged event to RadioPlayerService and updated PlayerViewModel to only rebuild LocalTrackList when the track list changes, preserving selection and scroll. Improved debug logging and performed minor cleanup and documentation updates.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant