Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.