Skip to content

v0.1.0 — Gapless playback, BPM detection, Artists view, speed control#7

Merged
devohmycode merged 3 commits intomainfrom
0.1.0
Mar 17, 2026
Merged

v0.1.0 — Gapless playback, BPM detection, Artists view, speed control#7
devohmycode merged 3 commits intomainfrom
0.1.0

Conversation

@devohmycode
Copy link
Owner

@devohmycode devohmycode commented Mar 17, 2026

Summary

  • Gapless playback — seamless track transitions via pre-loaded audio chain with GaplessSampleProvider; automatic fallback for incompatible formats
  • BPM detection — reads BPM from file tags on scan, or analyzes audio on demand (multi-band onset detection + comb filter); displayed in track list, sortable, written back to file tags
  • Artists view — browse library grouped by artist with circular artwork cards, detail view with search/sort
  • Albums view — browse library grouped by album with artwork cards
  • Playback speed — adjustable 0.25x–4x with 9 presets via SpeedControlSampleProvider (linear interpolation)
  • Podcast enhancements — episode downloads (offline playback), playback resume (progress persistence), unread episode badges on subscriptions
  • Sleep timer — auto-stop after 15/30/45/60/90 minutes
  • Folder drag & drop — drop folders onto the window to scan and add to library
  • Custom Acrylic ColorPicker — tint and fallback color squares now open a full WinUI ColorPicker flyout
  • Window drag fix — fixed collapse animation timer stuck in infinite loop preventing vertical window movement

New files

  • Services/GaplessSampleProvider.cs — ISampleProvider chain for gapless transitions
  • Services/SpeedControlSampleProvider.cs — playback speed via sample interpolation
  • Services/BpmDetector.cs — multi-band onset + comb filter BPM analysis

Test plan

  • Play a queue of same-format tracks and verify no gap between transitions
  • Right-click a track → Detect BPM → verify reasonable result (60–200 range)
  • Sort by BPM in library view
  • Navigate to Artists view via "..." menu, click an artist, verify detail view
  • Adjust playback speed, verify audio plays at correct rate
  • Download a podcast episode, close/reopen app, verify playback resumes
  • Settings → Custom Acrylic → click tint/fallback color square → verify ColorPicker opens
  • Drag window vertically after collapse/expand cycle → verify it moves freely

Summary by CodeRabbit

Release Notes

  • New Features

    • Playback speed control with preset options (0.5x–3.0x)
    • Gapless playback for seamless track transitions
    • BPM detection and sort-by-BPM capability
    • Podcast episode downloads with local playback and progress tracking
    • Configurable sleep timer (15–90 minutes)
    • Artists browsing view
    • Folder drag-and-drop support for library import
    • Unread episode badges on subscriptions
  • Chores

    • Version updated to 0.1.0

- Updated application version to 0.0.7 in installer.iss.
- Added playback speed control with adjustable presets.
- Implemented episode progress tracking for podcasts.
- Introduced download functionality for podcast episodes.
- Updated README with new features including playback speed, sleep timer, and episode downloads.
- Added .gitignore for SpecStory project files.
- Added BPM detection functionality using a new BpmDetector service.
- Introduced gapless playback support with a GaplessSampleProvider.
- Updated TrackInfo model to include BPM property.
- Enhanced audio player service to handle gapless transitions.
- Modified MainWindow to support sorting by BPM and display BPM in track metadata.
- Updated database schema to include BPM column in tracks table.
@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f3e5ddcb-bf8c-4c40-a18c-d77588a9fa79

📥 Commits

Reviewing files that changed from the base of the PR and between 2952df9 and e6c0ff7.

📒 Files selected for processing (17)
  • .gitignore
  • .specstory/.gitignore
  • .specstory/history/2026-03-13_11-23Z-untitled.md
  • .specstory/history/2026-03-13_13-06Z-inno-setup-installer-script.md
  • Audiomatic/Audiomatic.csproj
  • Audiomatic/MainWindow.xaml
  • Audiomatic/MainWindow.xaml.cs
  • Audiomatic/Models/TrackInfo.cs
  • Audiomatic/Services/AudioPlayerService.cs
  • Audiomatic/Services/BpmDetector.cs
  • Audiomatic/Services/GaplessSampleProvider.cs
  • Audiomatic/Services/LibraryManager.cs
  • Audiomatic/Services/PodcastService.cs
  • Audiomatic/Services/QueueManager.cs
  • Audiomatic/Services/SpeedControlSampleProvider.cs
  • README.md
  • installer.iss

📝 Walkthrough

Walkthrough

This PR enhances the audio player with BPM detection and metadata storage, gapless playback with pre-loaded next-track support, variable playback speed control, podcast episode downloading with progress resumption, artist browsing views, and sleep timer functionality. It also updates configuration, library schema with BPM column, and installer version to 0.1.0.

Changes

Cohort / File(s) Summary
Build & Configuration
.gitignore, Audiomatic/Audiomatic.csproj, installer.iss
Added publish/ to .gitignore; enabled self-contained publishing in csproj; bumped app version from 0.0.6 to 0.1.0.
Metadata & Documentation
.specstory/.gitignore, .specstory/history/...
Updated .specstory/.gitignore to ignore project metadata; added historical records of SpecStory build workflows and Inno Setup compiler invocations.
Data Model
Audiomatic/Models/TrackInfo.cs, Audiomatic/Services/LibraryManager.cs
Added Bpm property to TrackInfo; extended database schema with bpm column, migration logic, and updated INSERT/SELECT queries to persist and retrieve BPM metadata.
Core Audio Processing
Audiomatic/Services/AudioPlayerService.cs
Introduced gapless playback chain (_gaplessProvider, _nextAudioReader, _nextEqualizer, _nextSpeedProvider), speed control (_speedProvider, PlaybackSpeed property), gapless transition events/handler, and PrepareNextTrack method. Added RemainingSeconds property and speed propagation to streaming paths.
Gapless & Speed Providers
Audiomatic/Services/GaplessSampleProvider.cs, Audiomatic/Services/SpeedControlSampleProvider.cs
New GaplessSampleProvider queues and seamlessly transitions between audio sources with SourceTransitioned and PlaybackEnded events. New SpeedControlSampleProvider applies linear interpolation for variable playback speed (0.25x–4x range).
BPM Detection
Audiomatic/Services/BpmDetector.cs
New public BpmDetector utility with Detect(filePath) method: analyzes audio via 2-band spectral approach (bass <200 Hz + mid 200–5000 Hz), computes onset envelopes, and fuses CombFilter and Autocorrelation BPM estimates with harmonic normalization; returns 0 on failure.
Queue & Podcast Services
Audiomatic/Services/QueueManager.cs, Audiomatic/Services/PodcastService.cs
Added PeekNext() to QueueManager for non-destructive queue inspection. Extended PodcastService with progress tracking (LoadProgress/SaveProgress in podcast_progress.json), episode downloads with cancellation (DownloadEpisodeAsync, DeleteDownload), and utility methods for download paths and size queries.
UI XAML
Audiomatic/MainWindow.xaml
Added SpeedButton and SpeedText for playback speed display, ArtistHeader sections for artist navigation, and SortBpm items in sort flyouts for BPM-based sorting.
UI Code-Behind
Audiomatic/MainWindow.xaml.cs
Extended ViewMode enum with Artists and ArtistDetail. Added podcast progress/download state tracking, BPM metadata display and detection UI, speed control with presets (0.5x–3x), sleep timer with configurable duration, drag-and-drop folder imports, and artist-specific grid rendering. Updated navigation, detail headers, and view transitions to support new modes.
Documentation
README.md
Documented new features: gapless playback, BPM detection/sorting, playback speed control, sleep timer, episode downloads with resume capability, artist/album browsing, and custom acrylic backdrop options. Updated data storage and drag-and-drop interaction descriptions.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as MainWindow
    participant Queue as QueueManager
    participant Player as AudioPlayerService
    participant Gapless as GaplessSampleProvider
    participant Speed as SpeedControlSampleProvider
    participant Output as NAudio Output

    User->>UI: Request next track
    UI->>Queue: PeekNext()
    Queue-->>UI: Next TrackInfo
    UI->>Player: PrepareNextTrack(nextTrack)
    Player->>Player: Build audio chain for next
    Player->>Gapless: QueueNext(nextChain)
    Gapless-->>Player: Queued ✓

    Player->>Gapless: PlayTrack(currentChain)
    Gapless->>Speed: Read() [current track]
    Speed->>Output: Write samples
    
    Note over Gapless: Current track exhausted
    Gapless->>Speed: Read() [switch to next]
    Gapless->>Gapless: SourceTransitioned event
    Speed->>Output: Write next track samples
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Hops with glee at sonic speeds,
Fast and slow for all your needs,
Seamless tracks with nary a sound,
BPMs found and artists crowned!

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 0.1.0
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can enforce grammar and style rules using `languagetool`.

Configure the reviews.tools.languagetool setting to enable/disable rules and categories. Refer to the LanguageTool Community to learn more.

@qodo-code-review
Copy link

Review Summary by Qodo

v0.1.0 — Gapless playback, BPM detection, Artists view, speed control, and podcast enhancements

✨ Enhancement

Grey Divider

Walkthroughs

Description
• **Gapless playback** — seamless track transitions via GaplessSampleProvider with pre-loading and
  automatic fallback for incompatible formats
• **BPM detection** — multi-band onset detection with comb filter analysis; reads from file tags,
  analyzes audio on demand, displayed and sortable in track list, persisted to database
• **Playback speed control** — adjustable 0.25x–4x with 9 presets via SpeedControlSampleProvider
  using linear interpolation
• **Artists and Albums views** — browse library grouped by artist/album with circular artwork cards
  and detail views with search/sort capabilities
• **Sleep timer** — auto-stop after 15/30/45/60/90 minutes
• **Podcast enhancements** — episode downloads (offline playback), playback resume with progress
  persistence, unread episode badges on subscriptions
• **Folder drag & drop** — drop folders onto window to scan and add to library
• **Custom Acrylic ColorPicker** — enhanced color picker with full WinUI flyout for tint and
  fallback colors
• **Window drag fix** — resolved collapse animation timer preventing vertical window movement
• **Database schema updates** — added bpm column to tracks table with migration support
• **Audio chain integration** — SpeedControlSampleProvider and GaplessSampleProvider integrated
  into AudioPlayerService
Diagram
flowchart LR
  A["Audio Source"] --> B["GaplessSampleProvider"]
  B --> C["SpeedControlSampleProvider"]
  C --> D["Output"]
  E["BpmDetector"] -.-> F["Track Metadata"]
  G["PodcastService"] --> H["Episode Downloads"]
  G --> I["Progress Tracking"]
  J["LibraryManager"] --> K["BPM Storage"]
  L["UI Layer"] --> M["Artists View"]
  L --> N["Speed Control"]
  L --> O["Sleep Timer"]
Loading

Grey Divider

File Changes

1. Audiomatic/MainWindow.xaml.cs ✨ Enhancement +657/-35

Major feature release: Artists view, BPM detection, speed control, sleep timer

• Added Artists and ArtistDetail view modes with circular artwork cards and artist grouping
• Implemented BPM detection via context menu with multi-band onset analysis; BPM now sortable and
 displayed in track list
• Added playback speed control (0.25x–4x) with 9 presets accessible via flyout menu
• Implemented sleep timer (15/30/45/60/90 min) with auto-stop functionality
• Enhanced podcast support: episode downloads, playback resume from saved progress, unread badges on
 subscriptions
• Added gapless playback pre-loading and transition handling via GaplessSampleProvider
• Implemented folder drag-and-drop to add folders to library with automatic scanning
• Enhanced custom Acrylic color picker with full WinUI ColorPicker flyout for tint and fallback
 colors

Audiomatic/MainWindow.xaml.cs


2. Audiomatic/Services/BpmDetector.cs ✨ Enhancement +290/-0

New BPM detection service with multi-band analysis

• New static service for BPM detection using multi-band spectral flux onset detection
• Implements comb-filter and autocorrelation-based tempo estimation (60–200 BPM range)
• Analyzes 30s of audio after skipping first 20% to avoid intros
• Includes low-pass and band-pass filtering for bass/kick and mid-range isolation
• Adaptive thresholding to suppress low-energy noise

Audiomatic/Services/BpmDetector.cs


3. Audiomatic/Services/SpeedControlSampleProvider.cs ✨ Enhancement +96/-0

New speed control sample provider with interpolation

• New ISampleProvider implementation for playback speed control via linear interpolation
• Supports 0.25x–4x speed range with pitch shifting (vinyl-like effect)
• Includes internal buffering and frame compaction for efficient memory usage
• Provides Reset() method for seeking support

Audiomatic/Services/SpeedControlSampleProvider.cs


View more (12)
4. Audiomatic/Services/GaplessSampleProvider.cs ✨ Enhancement +106/-0

New gapless playback provider for seamless transitions

• New ISampleProvider wrapper enabling seamless gapless playback transitions
• Queues next source and switches when current source ends without gaps
• Thread-safe with lock-based synchronization for audio thread safety
• Fires SourceTransitioned and PlaybackEnded events for UI coordination

Audiomatic/Services/GaplessSampleProvider.cs


5. Audiomatic/Services/AudioPlayerService.cs ✨ Enhancement +153/-2

Audio player integration of speed and gapless providers

• Integrated SpeedControlSampleProvider and GaplessSampleProvider into audio chain
• Added PrepareNextTrack() method to pre-build audio chain for gapless transitions
• Implemented RemainingSeconds property for gapless pre-loading trigger
• Added PlaybackSpeed property with clamping (0.25–4x) and stream support
• Added GaplessTransitioned event fired when seamless transition occurs

Audiomatic/Services/AudioPlayerService.cs


6. Audiomatic/Services/LibraryManager.cs ✨ Enhancement +42/-11

Database schema and queries updated for BPM storage

• Added bpm column to tracks table with migration logic for existing databases
• Updated ReadMetadata() to extract BPM from file tags via TagLib
• Added UpdateTrackBpm() method to persist detected BPM to database
• Updated all track query SELECT statements to include bpm column

Audiomatic/Services/LibraryManager.cs


7. Audiomatic/Services/PodcastService.cs ✨ Enhancement +124/-0

Podcast episode downloads and progress persistence

• Added episode progress tracking: LoadProgress() and SaveProgress() for resume functionality
• Implemented episode downloads with DownloadEpisodeAsync(), IsDownloaded(), GetDownloadPath()
• Added DeleteDownload() and GetDownloadsSizeBytes() for download management
• Downloads stored in %LOCALAPPDATA%\Audiomatic\podcasts\ with SHA256-based filenames
• Supports NAudio-compatible formats (.mp3, .wav, .flac, .aac, .ogg, .m4a, .aiff)

Audiomatic/Services/PodcastService.cs


8. Audiomatic/Services/QueueManager.cs ✨ Enhancement +14/-0

Queue manager peek operation for gapless preparation

• Added PeekNext() method to return next track without advancing queue index
• Respects repeat modes (One, All, None) for peek operation

Audiomatic/Services/QueueManager.cs


9. Audiomatic/Models/TrackInfo.cs ✨ Enhancement +1/-0

Track model extended with BPM field

• Added Bpm property (int) to track model for BPM storage

Audiomatic/Models/TrackInfo.cs


10. Audiomatic/MainWindow.xaml ✨ Enhancement +18/-0

XAML UI updates for speed control and artist view

• Added SpeedButton and SpeedText UI elements for playback speed display and control
• Added ArtistHeader with back button and artist name display for detail view
• Added SortBpm menu item to sort options

Audiomatic/MainWindow.xaml


11. Audiomatic/Audiomatic.csproj ⚙️ Configuration changes +1/-0

Project configuration for self-contained build

• Added <SelfContained>true</SelfContained> to project configuration for self-contained deployment

Audiomatic/Audiomatic.csproj


12. installer.iss ⚙️ Configuration changes +1/-1

Installer version bump to 0.1.0

• Updated AppVersion from 0.0.6 to 0.1.0

installer.iss


13. README.md 📝 Documentation +15/-3

README updated with v0.1.0 feature documentation

• Added documentation for gapless playback, BPM detection, playback speed, and sleep timer features
• Added episode downloads, smart playback, playback resume, and unread badges to podcast section
• Updated sort options to include BPM
• Added Albums and Artists views to feature list
• Updated backdrop options documentation with custom Acrylic details
• Added folder drag-and-drop capability
• Added podcast progress and downloads directory to data storage section

README.md


14. .specstory/history/2026-03-13_13-06Z-inno-setup-installer-script.md Miscellaneous +278/-0

Build session history for installer compilation

• New SpecStory session log documenting Inno Setup compiler execution for v0.1.0 installer

.specstory/history/2026-03-13_13-06Z-inno-setup-installer-script.md


15. .specstory/history/2026-03-13_11-23Z-untitled.md Miscellaneous +18/-0

Build session history stub

• New SpecStory session log (minimal content)

.specstory/history/2026-03-13_11-23Z-untitled.md


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Mar 17, 2026

Code Review by Qodo

🐞 Bugs (8) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Speed buffer out-of-range 🐞 Bug ✓ Correctness
Description
SpeedControlSampleProvider.Read can index past _sourceBuffer for multi-channel audio because it only
guarantees samples up to the start of the next frame, but then reads all channels of that next
frame. This can throw IndexOutOfRangeException during non-1.0 playback speeds.
Code

Audiomatic/Services/SpeedControlSampleProvider.cs[R49-69]

+        for (int i = 0; i < outputFrames; i++)
+        {
+            int neededSample = ((int)_sourcePosition + 1) * _channels;
+            while (neededSample >= _sourceBufferCount)
+            {
+                EnsureCapacity(_sourceBufferCount + 4096);
+                int read = _source.Read(_sourceBuffer, _sourceBufferCount, 4096);
+                if (read == 0)
+                    return written;
+                _sourceBufferCount += read;
+            }
+
+            int srcFrame = (int)_sourcePosition;
+            float frac = (float)(_sourcePosition - srcFrame);
+
+            for (int ch = 0; ch < _channels; ch++)
+            {
+                float s0 = _sourceBuffer[srcFrame * _channels + ch];
+                float s1 = _sourceBuffer[(srcFrame + 1) * _channels + ch];
+                buffer[offset + written++] = s0 + (s1 - s0) * frac;
+            }
Evidence
The code ensures _sourceBufferCount exceeds (srcFrame+1)*channels but then accesses
(srcFrame+1)*channels + ch, which for ch>0 requires buffering the entire next frame (i.e., at least
(srcFrame+2)*channels samples). For stereo, the loop may exit with _sourceBufferCount==3 while still
reading index 3 (out of range).

Audiomatic/Services/SpeedControlSampleProvider.cs[49-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`SpeedControlSampleProvider.Read` can read beyond `_sourceBuffer` when interpolating the `(srcFrame + 1)` frame for multi-channel audio.

### Issue Context
The current guard uses `neededSample = (srcFrame+1)*channels` which only ensures the first sample index of the next frame is available, but interpolation reads all channels of that next frame.

### Fix Focus Areas
- Audiomatic/Services/SpeedControlSampleProvider.cs[49-69]

### Suggested change
Update the guard to ensure the full next frame exists, e.g. compute:
- `int neededSamples = (srcFrame + 2) * _channels;`
- while (`neededSamples &gt; _sourceBufferCount`) read more.
Also consider handling the end-of-source case by padding with zeros or duplicating last frame to avoid early-return artifacts.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Gapless next speed stale 🐞 Bug ✓ Correctness
Description
AudioPlayerService.PlaybackSpeed updates only the current speed provider; if a next track was
already prepared for gapless playback, its SpeedControlSampleProvider keeps the old speed. After the
gapless transition, playback speed can unexpectedly jump to the stale value.
Code

Audiomatic/Services/AudioPlayerService.cs[R562-572]

+    public float PlaybackSpeed
+    {
+        get => _playbackSpeed;
+        set
+        {
+            _playbackSpeed = Math.Clamp(value, 0.25f, 4.0f);
+            if (_speedProvider != null)
+                _speedProvider.Speed = _playbackSpeed;
+            if (!_useNAudio)
+                _mediaPlayer.PlaybackSession.PlaybackRate = _playbackSpeed;
+        }
Evidence
PrepareNextTrack snapshots _playbackSpeed into _nextSpeedProvider at prepare time, but later
PlaybackSpeed changes only apply to _speedProvider (current chain), not _nextSpeedProvider
(queued chain). When transitioning, _nextSpeedProvider becomes _speedProvider, carrying the
stale speed into the newly-playing track.

Audiomatic/Services/AudioPlayerService.cs[208-238]
Audiomatic/Services/AudioPlayerService.cs[562-572]
Audiomatic/Services/AudioPlayerService.cs[253-259]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
When `PrepareNextTrack` has already created `_nextSpeedProvider`, changing `PlaybackSpeed` only updates the current `_speedProvider` and not the queued `_nextSpeedProvider`, so speed can change unexpectedly after a gapless transition.

### Issue Context
`PrepareNextTrack` snapshots the current `_playbackSpeed` into `_nextSpeedProvider`.

### Fix Focus Areas
- Audiomatic/Services/AudioPlayerService.cs[562-572]
- Audiomatic/Services/AudioPlayerService.cs[208-238]

### Suggested change
In `PlaybackSpeed` setter, also update `_nextSpeedProvider.Speed` when present:
```csharp
if (_nextSpeedProvider != null)
   _nextSpeedProvider.Speed = _playbackSpeed;
```
Optionally, if a next track is queued and speed changes, consider re-queuing to avoid internal state mismatch.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Invalid podcast URL crash 🐞 Bug ⛯ Reliability
Description
PodcastService.GetDownloadPath constructs a Uri from the episode enclosure URL without validation,
and the UI calls IsDownloaded/GetDownloadPath for every episode. A malformed RSS enclosure URL can
throw UriFormatException and crash episode rendering/download checks.
Code

Audiomatic/Services/PodcastService.cs[R155-172]

+    public static string GetDownloadPath(string audioUrl)
+    {
+        // Use a hash of the URL to avoid filesystem issues with long/special-char URLs
+        var hash = Convert.ToHexString(
+            System.Security.Cryptography.SHA256.HashData(
+                System.Text.Encoding.UTF8.GetBytes(audioUrl)))[..24];
+        // Keep original extension only if NAudio supports it, otherwise default to .mp3
+        var ext = Path.GetExtension(new Uri(audioUrl).AbsolutePath);
+        if (string.IsNullOrEmpty(ext) || !SupportedDownloadExtensions.Contains(ext))
+            ext = ".mp3";
+        return Path.Combine(DownloadsDir, hash + ext);
+    }
+
+    public static bool IsDownloaded(string audioUrl)
+    {
+        var path = GetDownloadPath(audioUrl);
+        return File.Exists(path);
+    }
Evidence
FetchEpisodesAsync accepts the RSS enclosure "url" attribute as-is and does not validate it.
GetDownloadPath then calls new Uri(audioUrl) unguarded, and IsDownloaded calls GetDownloadPath, so
any invalid audioUrl will throw in normal UI flows (episode list rendering, context menu, etc.).

Audiomatic/Services/PodcastService.cs[51-75]
Audiomatic/Services/PodcastService.cs[155-172]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`PodcastService.GetDownloadPath` calls `new Uri(audioUrl)` without validation. If a feed provides a malformed enclosure URL, this throws and can crash the UI when calling `IsDownloaded`, `DeleteDownload`, etc.

### Issue Context
`FetchEpisodesAsync` stores enclosure URLs without validating they are absolute/valid.

### Fix Focus Areas
- Audiomatic/Services/PodcastService.cs[51-75]
- Audiomatic/Services/PodcastService.cs[155-172]

### Suggested change
Option A (preferred): make `GetDownloadPath` defensive:
- `if (!Uri.TryCreate(audioUrl, UriKind.Absolute, out var uri))` then fall back to parsing extension from the raw string (or default `.mp3`).
- Wrap extension extraction in try/catch.

Option B: validate in `FetchEpisodesAsync` before adding episodes (only add if `Uri.TryCreate(..., Absolute, out _)`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. UI-thread progress file writes 🐞 Bug ➹ Performance
Description
MainWindow.OnPositionChanged saves podcast progress by calling PodcastService.SaveProgress on the UI
thread every ~5 seconds. SaveProgress performs synchronous JSON serialization and File.WriteAllText,
which can cause periodic UI stutter during podcast playback.
Code

Audiomatic/MainWindow.xaml.cs[R643-649]

+        // Save podcast episode progress every ~5s (20 ticks × 250ms)
+        if (_currentEpisode != null && pos.TotalSeconds > 1 && ++_progressSaveCounter >= 20)
+        {
+            _progressSaveCounter = 0;
+            _episodeProgress[_currentEpisode.AudioUrl] = pos.TotalSeconds;
+            PodcastService.SaveProgress(_episodeProgress);
+        }
Evidence
PositionChanged is dispatched onto the UI thread every 250ms from AudioPlayerService’s timer;
OnPositionChanged then does synchronous disk I/O via PodcastService.SaveProgress. This introduces
periodic blocking work on the UI thread while playing podcasts.

Audiomatic/Services/AudioPlayerService.cs[119-123]
Audiomatic/MainWindow.xaml.cs[643-649]
Audiomatic/Services/PodcastService.cs[99-106]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Podcast progress persistence does synchronous `File.WriteAllText` from the UI thread every ~5 seconds.

### Issue Context
`PositionChanged` is enqueued to the UI thread from a 250ms timer; `OnPositionChanged` calls `PodcastService.SaveProgress` which serializes and writes JSON synchronously.

### Fix Focus Areas
- Audiomatic/MainWindow.xaml.cs[643-649]
- Audiomatic/Services/PodcastService.cs[99-106]
- Audiomatic/Services/AudioPlayerService.cs[119-123]

### Suggested change
Implement a debounced background writer:
- Update `_episodeProgress` in-memory on UI thread.
- Queue a background task (single-flight) that writes after N seconds of inactivity, or write using `await File.WriteAllTextAsync` from a background thread.
- Ensure last progress is flushed on app close (existing `SavePodcastProgressNow`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Gapless audio-thread starvation 🐞 Bug ⛯ Reliability
Description
GaplessSampleProvider invokes SourceTransitioned from inside Read while holding its lock and
explicitly states it runs on the audio thread. AudioPlayerService.OnGaplessTransition calls
UpdateSmtc synchronously in that callback, risking audible glitches due to blocking work on the
audio thread.
Code

Audiomatic/Services/GaplessSampleProvider.cs[R71-100]

+    public int Read(float[] buffer, int offset, int count)
+    {
+        lock (_lock)
+        {
+            if (_current == null) return 0;
+
+            var read = _current.Read(buffer, offset, count);
+
+            if (read < count)
+            {
+                // Current source ended — try to switch to next
+                if (_next != null)
+                {
+                    _current = _next;
+                    _next = null;
+                    _currentEnded = false;
+
+                    // Fill remaining buffer from the new source
+                    var remaining = count - read;
+                    var nextRead = _current.Read(buffer, offset + read, remaining);
+                    read += nextRead;
+
+                    // Fire transition event (on audio thread — keep it fast)
+                    SourceTransitioned?.Invoke();
+                }
+                else if (!_currentEnded && read == 0)
+                {
+                    _currentEnded = true;
+                    PlaybackEnded?.Invoke();
+                }
Evidence
The gapless provider fires transition callbacks on the audio render thread and does so while holding
its internal lock. The subscribed handler performs non-trivial work (SMTC updates and allocations)
before returning, which can delay audio reads and cause dropouts at the transition boundary.

Audiomatic/Services/GaplessSampleProvider.cs[18-22]
Audiomatic/Services/GaplessSampleProvider.cs[71-100]
Audiomatic/Services/AudioPlayerService.cs[169-171]
Audiomatic/Services/AudioPlayerService.cs[271-279]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Gapless transition callbacks run on the audio thread and are invoked under a lock. The current handler updates SMTC synchronously, which can starve the audio thread during the critical transition window.

### Issue Context
`GaplessSampleProvider` documents `SourceTransitioned` runs on the audio thread.

### Fix Focus Areas
- Audiomatic/Services/GaplessSampleProvider.cs[71-100]
- Audiomatic/Services/AudioPlayerService.cs[245-282]

### Suggested change
- In `GaplessSampleProvider.Read`, capture whether transition happened and invoke `SourceTransitioned` *after* leaving the lock.
- In `AudioPlayerService.OnGaplessTransition`, avoid calling `UpdateSmtc` directly; instead `TryEnqueue` all transition work (including SMTC updates) onto the dispatcher thread, and keep the audio-thread callback minimal.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Artist art not clipped 🐞 Bug ✓ Correctness
Description
BuildArtistsGrid creates clip geometries for circular artwork but never applies them to the image or
container. As a result, artist artwork will not render as circular cards as intended.
Code

Audiomatic/MainWindow.xaml.cs[R3040-3063]

+            // Clip to circle
+            var clip = new RectangleGeometry
+            {
+                Rect = new Windows.Foundation.Rect(0, 0, 142, 142)
+            };
+            var ellipseClip = new Microsoft.UI.Xaml.Media.EllipseGeometry
+            {
+                Center = new Windows.Foundation.Point(71, 71),
+                RadiusX = 71,
+                RadiusY = 71
+            };
+
+            var placeholder = new FontIcon
+            {
+                Glyph = "\uE77B",
+                FontSize = 36,
+                HorizontalAlignment = HorizontalAlignment.Center,
+                VerticalAlignment = VerticalAlignment.Center,
+                Foreground = ThemeHelper.Brush("TextFillColorTertiaryBrush")
+            };
+
+            artGrid.Children.Add(placeholder);
+            artGrid.Children.Add(artImage);
+            card.Children.Add(artGrid);
Evidence
The method allocates RectangleGeometry and EllipseGeometry objects but no element’s Clip property is
set, and the geometries are never referenced afterwards. This contradicts the stated goal of
circular artwork cards and leaves dead code behind.

Audiomatic/MainWindow.xaml.cs[3023-3050]
Audiomatic/MainWindow.xaml.cs[3061-3063]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Artists view intends circular artwork but does not apply any clipping; clip geometry variables are unused.

### Issue Context
`ellipseClip` is constructed but never assigned.

### Fix Focus Areas
- Audiomatic/MainWindow.xaml.cs[3023-3063]

### Suggested change
- Assign `artImage.Clip = ellipseClip;` (and ensure it updates if size changes).
- Remove the unused `clip` variable if not needed.
- Alternatively, replace `Grid` with a `Border` (`CornerRadius=71`) and set `border.Child = artImage` to get consistent clipping.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
7. SpecStory logs committed 🐞 Bug ⛨ Security
Description
The PR adds .specstory/history markdown files containing local absolute paths and build logs. This
leaks developer-environment information and adds non-product artifacts to the repository.
Code

.specstory/history/2026-03-13_11-23Z-untitled.md[R7-11]

+_**User**_
+
+"C:\Users\Dev\AppData\Local\Programs\Inno Setup 6\ISCC.exe" installer.iss
+
+---
Evidence
The committed SpecStory files include absolute Windows paths and command invocations that are
unrelated to application functionality and may expose developer machine details.

.specstory/history/2026-03-13_11-23Z-untitled.md[7-11]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
SpecStory session history files were committed and include local absolute paths/logs.

### Issue Context
These are editor artifacts, not product code.

### Fix Focus Areas
- .specstory/history/2026-03-13_11-23Z-untitled.md[1-18]
- .specstory/history/2026-03-13_13-06Z-inno-setup-installer-script.md[1-278]

### Suggested change
- Delete the committed `.specstory/history/*` files.
- Add `.specstory/` to the repo’s `.gitignore` (top-level) to prevent reintroduction.
- If the folder must exist for tooling, keep only a placeholder and ignore the rest.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

8. Speed presets mismatch docs 🐞 Bug ✓ Correctness
Description
README documents 0.25x–4x playback speed with 9 presets, but the UI exposes presets only from
0.5x–3.0x. Users cannot select the documented minimum/maximum speeds through the UI.
Code

Audiomatic/MainWindow.xaml.cs[R917-920]

+    // -- Playback Speed -------------------------------------------
+
+    private static readonly float[] SpeedPresets = [0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.5f, 3.0f];
+
Evidence
Documentation states 0.25x–4x with 9 presets, but the in-app preset list omits 0.25x and 4x despite
the backend supporting that clamp range.

README.md[15-25]
Audiomatic/MainWindow.xaml.cs[917-920]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Playback speed presets in the UI don’t match the documented range.

### Issue Context
Backend clamps 0.25x–4x, README claims 0.25x–4x with 9 presets, but UI presets are 0.5x–3.0x.

### Fix Focus Areas
- Audiomatic/MainWindow.xaml.cs[917-920]
- README.md[15-25]

### Suggested change
Decide source of truth:
- If README is correct, change presets to include 0.25x and 4.0x (and adjust count/values accordingly).
- If UI is correct, update README text to 0.5x–3.0x.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@devohmycode devohmycode merged commit dadbdf2 into main Mar 17, 2026
1 check was pending
Comment on lines +49 to +69
for (int i = 0; i < outputFrames; i++)
{
int neededSample = ((int)_sourcePosition + 1) * _channels;
while (neededSample >= _sourceBufferCount)
{
EnsureCapacity(_sourceBufferCount + 4096);
int read = _source.Read(_sourceBuffer, _sourceBufferCount, 4096);
if (read == 0)
return written;
_sourceBufferCount += read;
}

int srcFrame = (int)_sourcePosition;
float frac = (float)(_sourcePosition - srcFrame);

for (int ch = 0; ch < _channels; ch++)
{
float s0 = _sourceBuffer[srcFrame * _channels + ch];
float s1 = _sourceBuffer[(srcFrame + 1) * _channels + ch];
buffer[offset + written++] = s0 + (s1 - s0) * frac;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Speed buffer out-of-range 🐞 Bug ✓ Correctness

SpeedControlSampleProvider.Read can index past _sourceBuffer for multi-channel audio because it only
guarantees samples up to the start of the next frame, but then reads all channels of that next
frame. This can throw IndexOutOfRangeException during non-1.0 playback speeds.
Agent Prompt
### Issue description
`SpeedControlSampleProvider.Read` can read beyond `_sourceBuffer` when interpolating the `(srcFrame + 1)` frame for multi-channel audio.

### Issue Context
The current guard uses `neededSample = (srcFrame+1)*channels` which only ensures the first sample index of the next frame is available, but interpolation reads all channels of that next frame.

### Fix Focus Areas
- Audiomatic/Services/SpeedControlSampleProvider.cs[49-69]

### Suggested change
Update the guard to ensure the full next frame exists, e.g. compute:
- `int neededSamples = (srcFrame + 2) * _channels;`
- while (`neededSamples > _sourceBufferCount`) read more.
Also consider handling the end-of-source case by padding with zeros or duplicating last frame to avoid early-return artifacts.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +562 to +572
public float PlaybackSpeed
{
get => _playbackSpeed;
set
{
_playbackSpeed = Math.Clamp(value, 0.25f, 4.0f);
if (_speedProvider != null)
_speedProvider.Speed = _playbackSpeed;
if (!_useNAudio)
_mediaPlayer.PlaybackSession.PlaybackRate = _playbackSpeed;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Gapless next speed stale 🐞 Bug ✓ Correctness

AudioPlayerService.PlaybackSpeed updates only the current speed provider; if a next track was
already prepared for gapless playback, its SpeedControlSampleProvider keeps the old speed. After the
gapless transition, playback speed can unexpectedly jump to the stale value.
Agent Prompt
### Issue description
When `PrepareNextTrack` has already created `_nextSpeedProvider`, changing `PlaybackSpeed` only updates the current `_speedProvider` and not the queued `_nextSpeedProvider`, so speed can change unexpectedly after a gapless transition.

### Issue Context
`PrepareNextTrack` snapshots the current `_playbackSpeed` into `_nextSpeedProvider`.

### Fix Focus Areas
- Audiomatic/Services/AudioPlayerService.cs[562-572]
- Audiomatic/Services/AudioPlayerService.cs[208-238]

### Suggested change
In `PlaybackSpeed` setter, also update `_nextSpeedProvider.Speed` when present:
```csharp
if (_nextSpeedProvider != null)
    _nextSpeedProvider.Speed = _playbackSpeed;
```
Optionally, if a next track is queued and speed changes, consider re-queuing to avoid internal state mismatch.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +155 to +172
public static string GetDownloadPath(string audioUrl)
{
// Use a hash of the URL to avoid filesystem issues with long/special-char URLs
var hash = Convert.ToHexString(
System.Security.Cryptography.SHA256.HashData(
System.Text.Encoding.UTF8.GetBytes(audioUrl)))[..24];
// Keep original extension only if NAudio supports it, otherwise default to .mp3
var ext = Path.GetExtension(new Uri(audioUrl).AbsolutePath);
if (string.IsNullOrEmpty(ext) || !SupportedDownloadExtensions.Contains(ext))
ext = ".mp3";
return Path.Combine(DownloadsDir, hash + ext);
}

public static bool IsDownloaded(string audioUrl)
{
var path = GetDownloadPath(audioUrl);
return File.Exists(path);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Invalid podcast url crash 🐞 Bug ⛯ Reliability

PodcastService.GetDownloadPath constructs a Uri from the episode enclosure URL without validation,
and the UI calls IsDownloaded/GetDownloadPath for every episode. A malformed RSS enclosure URL can
throw UriFormatException and crash episode rendering/download checks.
Agent Prompt
### Issue description
`PodcastService.GetDownloadPath` calls `new Uri(audioUrl)` without validation. If a feed provides a malformed enclosure URL, this throws and can crash the UI when calling `IsDownloaded`, `DeleteDownload`, etc.

### Issue Context
`FetchEpisodesAsync` stores enclosure URLs without validating they are absolute/valid.

### Fix Focus Areas
- Audiomatic/Services/PodcastService.cs[51-75]
- Audiomatic/Services/PodcastService.cs[155-172]

### Suggested change
Option A (preferred): make `GetDownloadPath` defensive:
- `if (!Uri.TryCreate(audioUrl, UriKind.Absolute, out var uri))` then fall back to parsing extension from the raw string (or default `.mp3`).
- Wrap extension extraction in try/catch.

Option B: validate in `FetchEpisodesAsync` before adding episodes (only add if `Uri.TryCreate(..., Absolute, out _)`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant