Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
6395702
feat(audio): add AudioFader service draft
httperry Aug 10, 2026
898da36
feat(audio): integrate AudioFader into pause controls
httperry Aug 10, 2026
a8be89a
pref: add audio fading settings to SettingsManager
httperry Aug 10, 2026
6250814
ui: draft initial NowPlayingSpotlightView layout
httperry Aug 10, 2026
969c5b5
ui: enhance NowPlayingSpotlightView with ambient glow and scrubber
httperry Aug 10, 2026
3fdd26f
feat(audio): add AudioFaderService with logarithmic volume curves
httperry Aug 10, 2026
f87ff9a
ui: add SpotlightHeaderView component
httperry Aug 10, 2026
77eaa9f
ui: add SpotlightControlsSection component
httperry Aug 10, 2026
fd48786
ui: add SpotlightSideDrawer component
httperry Aug 10, 2026
6356053
ui: assemble modular Spotlight view components in NowPlayingSpotlight…
httperry Aug 10, 2026
9b409a6
pref: add AudioFadingSettingsSection view
httperry Aug 10, 2026
13a0c00
test: add AudioFaderServiceTests unit test suite
httperry Aug 10, 2026
4952a6c
ui: add SpotlightAirPlayPickerView component
httperry Aug 10, 2026
1f866f2
style: format Swift code and resolve SwiftLint trailing closure viola…
httperry Aug 10, 2026
90d2cce
feat(audio): add AudioFadeCoordinator service
httperry Aug 10, 2026
72e4ae2
ui: add SpotlightLyricsDisplayView component
httperry Aug 10, 2026
db46065
ui: add SpotlightQueueListView component
httperry Aug 10, 2026
2450dcd
ui: add SpotlightKeyboardShortcutsView component
httperry Aug 10, 2026
fd6e070
ui: add SpotlightVolumeSliderView component
httperry Aug 10, 2026
9e8e0d3
ui: add SpotlightBackdropVisualizerView component
httperry Aug 10, 2026
f6a7339
feat(airplay): add SpotlightAirPlayRouteManager service
httperry Aug 10, 2026
540ce3a
feat(spotlight): integrate NowPlayingSpotlightView presentation into …
httperry Aug 10, 2026
996a086
feat(playerbar): open NowPlayingSpotlightView when clicking album thu…
httperry Aug 10, 2026
3184628
fix(audio): resolve audio fade timer race condition and restore volum…
httperry Aug 12, 2026
c9c0b69
refactor(spotlight): redesign NowPlayingSpotlightView with side-by-si…
httperry Aug 12, 2026
b630680
fix(playback): trigger DOM play-pause button and movie_player pauseVi…
httperry Aug 12, 2026
1163ff2
fix(audio): silence enforceVolumeNow feedback loop during active audi…
httperry Aug 12, 2026
13e2d68
fix(audio): remove double-toggle pause bug and use natural linear aud…
httperry Aug 12, 2026
07653a4
fix(audio): suppress autoplay recovery and volume enforcement during …
httperry Aug 12, 2026
44f7903
fix(audio): block pause() re-entry during active fade via isFadeOutIn…
httperry Aug 12, 2026
e16c5d4
feat(audio): implement Equal-Power Cosine/Sine curves for natural mus…
httperry Aug 12, 2026
99f3912
refactor(settings): integrate audio fading toggle and duration slider…
httperry Aug 12, 2026
5edbdc7
fix(playerbar): restore original Go to Album behavior on album thumbn…
httperry Aug 12, 2026
e47d7cb
style(player): upgrade PlayerBar backdrop with Liquid Glass gradient …
httperry Aug 15, 2026
ea1f461
fix(sidebar): add top safe inset and remove redundant system sidebar …
httperry Aug 15, 2026
152abab
feat(ui): implement seamless Liquid Glass topbar with fullscreen auto…
httperry Aug 15, 2026
0a1182d
fix(topbar): restore native unified window titlebar and natural sideb…
httperry Aug 15, 2026
9da9799
style(topbar): streamline single Liquid Glass topbar with Spotify-sty…
httperry Aug 15, 2026
2106bb0
style(layout): tighten top spacing and shift app content up
httperry Aug 15, 2026
7b4a3ba
feat(topbar): add in-app Liquid Glass Fullscreen toggle button and re…
httperry Aug 15, 2026
04ff74b
style(layout): align topbar vertically with traffic lights and shift …
httperry Aug 15, 2026
f6825c7
fix(layout): adjust topbar spacing in fullscreen to fix hit testing a…
httperry Aug 15, 2026
1a44fb6
fix: use windowToolbarFullScreenVisibility to allow clicks in fullscreen
httperry Aug 15, 2026
9c13578
fix: revert isFullScreen padding hacks and apply windowToolbarFullScr…
httperry Aug 15, 2026
f09e150
fix: change windowToolbarFullScreenVisibility from hidden to onHover
httperry Aug 15, 2026
b0dc52e
refactor(window): eliminate manual traffic light positioning and inte…
httperry Aug 16, 2026
9297ebd
fix(layout): add safe area top clearance to detail views and eliminat…
httperry Aug 16, 2026
02c78eb
fix(layout): remove allowsFullHeightLayout to fix player bottom blur,…
httperry Aug 16, 2026
1d9d25f
fix(fullscreen): collapse sidebar, add top margin, fix content cleara…
httperry Aug 16, 2026
330c434
fix(fullscreen): fix unclickable green button and content shift
httperry Aug 16, 2026
a245ddc
fix(titlebar): implement Spotify AppKit toolbar pattern and eliminate…
httperry Aug 16, 2026
b18ce44
fix(fullscreen): keep custom topbar controls visible and remove gray …
httperry Aug 16, 2026
dbff6ad
feat(titlebar): implement dual-bar approach — native toolbar + fullsc…
httperry Aug 17, 2026
2939bfe
fix(ui): restore topbar liquid glass blur, style toolbar buttons, and…
httperry Aug 17, 2026
4519063
fix(ui): center location pill and reduce detail content top padding
httperry Aug 17, 2026
4717997
fix(ui): eliminate duplicate toolbar navigation titles and enhance to…
httperry Aug 17, 2026
eb9e458
fix(ui): eliminate optical lens distortion on scroll underlays
httperry Aug 17, 2026
f3581e0
fix(ui): tighten top blur height to 54pt and eliminate heavy fog diff…
httperry Aug 17, 2026
7f34d98
feat(ui): restore Liquid Glass with subtle prismatic chromatic sheen …
httperry Aug 17, 2026
005ecc7
feat(ui): implement smooth frosted glass with cubic gradient fade and…
httperry Aug 17, 2026
ab3c529
fix(ui): smoother frosted glass fade — 8-stop easing curve, no harsh …
httperry Aug 17, 2026
b92c393
fix(ui): use ultraThinMaterial for topbar blur — eliminates glassEffe…
httperry Aug 17, 2026
5d6a534
fix(ui): natural blur — solid material over toolbar zone, tiny 14pt f…
httperry Aug 17, 2026
6535461
fix(ui): remove chunk bar and restore continuous natural frosted blur
httperry Aug 17, 2026
36d345d
feat(ui): implement true variable Gaussian blur using CABackdropLayer…
httperry Aug 17, 2026
78dad73
feat(ui): extend top blur to 78pt with smooth cubic fade and add topb…
httperry Aug 17, 2026
498c40d
feat(ui): refine top bar blur fade and add tint toggle via Location Pill
httperry Aug 17, 2026
275e01f
fix(ui): eliminate sharp compatGlass cutoff and ensure pill tap works…
httperry Aug 17, 2026
23ca1d1
fix(ui): ultra-smooth natural blur with enableTint toggle
httperry Aug 17, 2026
7c74f9c
feat(ui): smooth out top glass tint layers and mask for buttery elega…
httperry Aug 17, 2026
71ce285
fix(ui): remove intermediate gradient stops to eliminate visual bandi…
httperry Aug 17, 2026
939d55c
fix(ui): remove pink tint and use perfect continuous gradient without…
httperry Aug 17, 2026
35925c6
fix(ui): use perfect cubic smoothstep mask and extend frame height to…
httperry Aug 17, 2026
4150ac6
style(ui): shrink top bar height to 72pt and re-calculate smoothstep …
httperry Aug 17, 2026
d987124
fix(ui): shrink top bar to 64pt, add alpha buffer zone to prevent edg…
httperry Aug 17, 2026
cdec788
fix(ui): eliminate white tint strip by uniformly tinting glass and fa…
httperry Aug 17, 2026
c0bad94
style(ui): soften top bar tint to 50% opacity at top and smoothly fad…
httperry Aug 17, 2026
f981eb1
style(ui): set top bar mask to 50% transparency at top fading smoothl…
httperry Aug 17, 2026
bed9474
style(ui): restore pure liquid glass with 25% top transparency (75% o…
httperry Aug 17, 2026
671d556
style(ui): expand top bar height to 68pt for a slightly wider, more c…
httperry Aug 17, 2026
f408207
style(ui): ensure top bar tint adapts dynamically: black in dark mode…
httperry Aug 17, 2026
ce97d91
style(ui): increase liquid glass height to 84pt for more pronounced g…
httperry Aug 17, 2026
00121d1
style(ui): separate liquid glass mask (full strength) from color tint…
httperry Aug 17, 2026
f8fbfd2
style(ui): update bottom player area fade in PlayerBar and YouTubePla…
httperry Aug 17, 2026
1c3a6a6
style(ui): tighten bottom player area fade height to 135pt for sleeke…
httperry Aug 17, 2026
82cf9df
refactor(ui): extract reusable LiquidGlassFade component in SharedVie…
httperry Aug 17, 2026
e1005d5
style(ui): add vertical top padding to toolbar controls and topBarVie…
httperry Aug 17, 2026
a3f7785
style(ui): accent location pill icon with brand red color to match si…
httperry Aug 17, 2026
81ed638
fix(ui): preserve sidebar visibility state on entering and exiting fu…
httperry Aug 17, 2026
4731695
merge: sync upstream changes (Home refresh on demand #425)
httperry Aug 18, 2026
7ea5993
style(ui): apply smooth ease-out curve with feathered tail to LiquidG…
httperry Aug 18, 2026
97796f0
feat(audio): implement in-engine audio fading and natural footer alig…
httperry Aug 18, 2026
b650cee
merge: sync upstream changes (#447, #448)
httperry Aug 22, 2026
c8654c7
docs: update README for feature/audio-fade with in-engine audio fadin…
httperry Aug 22, 2026
50e00c2
docs: use standalone prominent app icon in audio-fade README
httperry Aug 22, 2026
deb4376
perf(docs): update compressed screenshot assets
httperry Aug 22, 2026
59d2a1d
fix(docs): restore lossless screenshots
httperry Aug 22, 2026
37d7bfe
docs: sync full-quality screenshot assets
httperry Aug 22, 2026
1ecec4b
updated ReadMe
httperry Aug 23, 2026
5116c95
updated ReadMe
httperry Aug 23, 2026
42dbbb2
Add Audio Files for fading comparision
httperry Aug 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# Meta / Tooling
.gitignore
.agents/
.shreakter.json
projectdata/
test_ui/
Assets/


# Build artifacts
.build/
DerivedData/
Expand Down Expand Up @@ -66,3 +75,4 @@ TEMP_*
build/
.codex/environments/environment.toml
Build
.gitignore
132 changes: 69 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,93 +1,99 @@
<h1 align="center">Kaset</h1>
<img src="docs/screenshots/app-icon.png" width="84" height="84" alt="Kaset App Icon">

<p align="center">A native macOS client for YouTube Music and YouTube, built with Swift and SwiftUI.</p>
# Kaset — In-Engine Audio Fading

<p align="center">
<a href="https://trendshift.io/repositories/16570?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-16570"><img src="https://trendshift.io/api/badge/repositories/16570" alt="sozercan/kaset | Trendshift" width="200" height="44"/></a>
Smooth volume fading and crossfading transitions for Kaset on macOS.

---

## Overview

Abrupt stops and starts during audio playback can feel jarring. This branch adds smooth audio fading directly into the player, easing the volume down when you pause or skip and fading it back in when you hit play.

<p align="left">
<img src="docs/screenshots/settings-audio-dark.png" alt="Audio Fading Settings" width="600">
</p>

---

## Audio Comparison: Fade vs. No Fade

Listen to the difference in playback transitions:

<table>
<tr>
<th>YouTube Music</th>
<th>YouTube</th>
<th width="50%">With In-Engine Audio Fade</th>
<th width="50%">Without Audio Fade (Abrupt Cut)</th>
</tr>
<tr>
<td valign="top">
<audio controls src="docs/audio/fade.wav"></audio>
<br><br>
<a href="docs/audio/fade.wav">Listen / Download <code>fade.wav</code></a>
</td>
<td valign="top">
<audio controls src="docs/audio/no-fade.wav"></audio>
<br><br>
<a href="docs/audio/no-fade.wav">Listen / Download <code>no-fade.wav</code></a>
</td>
</tr>
<tr>
<td><img src="docs/screenshot-ytm.png" alt="Kaset YouTube Music screenshot"></td>
<td><img src="docs/screenshot-yt.png" alt="Kaset YouTube screenshot"></td>
<td valign="top">Volume smoothly eases down when pausing and fades back in naturally on play.</td>
<td valign="top">Audio immediately cuts out with an abrupt stop.</td>
</tr>
</table>

## Features

### Music & Video
---

- 🎵 **Native macOS Experience** — Apple Music-style UI with Liquid Glass player bars, clean sidebar navigation, and a source toggle for Music ↔ YouTube
- 🎧 **YouTube Music Support** — Full playback of DRM-protected YouTube Music content via your existing Premium subscription
- ▶️ **[YouTube Support](docs/youtube.md)** — Browse regular YouTube recommendations, search, subscriptions, Shorts, Watch Later, history, comments, and video playback with native controls, captions, quality selection, and picture in picture
## Fading Curves & Mathematics

### Playback
Human hearing perceives volume changes on a curve rather than in a straight line. If you change volume linearly, the music feels like it drops too quickly at the start and hangs around too long at the end. To make transitions feel natural, Kaset uses smooth curve formulas for fading.

- 🎚️ **Equalizer** — System-wide 6-band parametric EQ with Spotify-style presets, applied to WebKit playback output
- 📜 **Lyrics** — View plain and synced lyrics with line-by-line highlighting when timing data is available, plus AI-powered explanations and mood analysis on macOS 26+
- 📃 **Queue Management** — View, reorder, shuffle, and clear your playback queue
- 🔀 **Smart Shuffle** — Beyond plain shuffle: blends suggested tracks into the queue based on what you're playing, with cadence and how many are queued ahead configurable in Settings
- 🔊 **Background Audio** — Music continues playing when the window is closed; stops on quit
- 🎶 **Track Notifications** — Get notified when a new track starts playing
For elapsed time $t$ over a target fade duration $T$, progress $p$ from start to finish is:

### Library & Discovery
$$
p = \min\left(1.0, \frac{t}{T}\right) \quad \text{where } p \in [0, 1]
$$

- 📚 **Library Access** — Browse playlists, liked songs, and subscribed podcasts; create playlists, add songs to playlists, and delete your own playlists
- 🧭 **Explore** — Discover new releases, charts, and moods & genres
- 🎙️ **Podcasts** — Browse and listen to podcasts with episode progress tracking
- 🔍 **Search** — Find songs, albums, artists, playlists, and podcasts
- 🕓 **History** — Revisit recently played tracks
### Volume Rise (Fade-In)
When starting or resuming playback, the volume ramps up from starting level $V_{\text{start}}$ to your set volume $V_{\text{target}}$:

### macOS Integration
$$
V_{\text{in}}(p) = V_{\text{start}} + (V_{\text{target}} - V_{\text{start}}) \cdot p^{\,2.2}
$$

- 🎛️ **System Integration** — Now Playing in Control Center, media key support, Dock menu controls
- ✨ **Apple Intelligence** — On-device AI for natural language commands, lyrics explanations, and playlist refinement on macOS 26+
- ⌨️ **[Keyboard Shortcuts](docs/keyboard-shortcuts.md)** — Full keyboard control for playback, navigation, and more
- 📳 **Haptic Feedback** — Tactile feedback on Force Touch trackpads for player controls and navigation
- 📣 **Share** — Share songs, playlists, albums, and artists via the native macOS share sheet
- 🌍 **Localized** — UI available in 17 languages (Arabic, Chinese (Simplified), Chinese (Traditional), Dutch, English, French, German, Indonesian, Italian, Korean, Polish, Portuguese, Russian, Spanish, Swedish, Turkish, Ukrainian); change under Settings → General → Language
Using an exponent of $\gamma = 2.2$ gives a gentle initial start that rises smoothly to your normal volume without sudden jumps.

### Automation & Extensibility
### Volume Decay (Fade-Out)
When pausing, skipping, or seeking, the volume fades down to zero:

- 🧩 **[Extensions](docs/extensions.md)** — Load WebKit Web Extensions, including [uBlock Origin Lite](https://github.com/uBlockOrigin/uBOL-home) and [SponsorBlock](https://github.com/ajayyy/SponsorBlock)
- 🔗 **[URL Scheme](docs/url-scheme.md)** — Open songs directly with `kaset://play?v=VIDEO_ID`; app-targeted YouTube watch and `youtu.be` links play in YouTube mode
- 🤖 **[AppleScript Support](docs/applescript.md)** — Automate playback with scripts, Raycast, Alfred, and Shortcuts
$$
V_{\text{out}}(p) = V_{\text{start}} \cdot (1 - p)^{2.0}
$$

## Requirements
A curve exponent of $\gamma = 2.0$ makes sure the music smoothly fades out to silence without any harsh cutoffs at the end.

- macOS 15.4 or later
- Apple Intelligence features require macOS 26.0 or later
- [Google](https://accounts.google.com) account for YouTube Music and YouTube personalization
---

## Installation
## Key Features

### Download
- **Natural Volume Curves**: Uses smooth curve math calibrated for human hearing so fades sound natural.
- **Glitch-Free Controls**: Handles rapid play/pause clicks and quick skips smoothly without volume spikes or audio glitches.
- **Customizable Preferences**: Easily adjust fade duration or turn it off completely in Settings → Music → Audio.
- **Automated Tests**: Backed by unit tests covering curve calculations, rapid button presses, and volume restoration.

Download the latest release from the [Releases](https://github.com/sozercan/kaset/releases) page.
---

### Homebrew
## Building from Source

```bash
brew install sozercan/repo/kaset
```

> **Note:** The app is not signed.
> If you downloaded the app manually, you can clear extended attributes (including quarantine) with:
>
> ```bash
> xattr -cr /Applications/Kaset.app
> ```
# Clone the feature branch
git clone -b feature/audio-fade https://github.com/httperry/Kaset.git
cd Kaset

## Contributing
# Build the project
swift build

See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, architecture, and coding guidelines.

We welcome AI-assisted contributions! You can submit traditional PRs or **prompt requests** — share the AI prompt that generates your changes, and maintainers can review the intent before running the code. See the [AI-Assisted Contributions](CONTRIBUTING.md#ai-assisted-contributions--prompt-requests) section for details.

## Disclaimer

Kaset is an unofficial application and not affiliated with YouTube or Google Inc. in any way. "YouTube", "YouTube Music" and the "YouTube Logo" are registered trademarks of Google Inc.
# Package and run the app bundle
./Scripts/compile_and_run.sh --debug
```
26 changes: 26 additions & 0 deletions Sources/Kaset/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,32 @@ extension AppDelegate: NSWindowDelegate {
sender.orderOut(nil)
return false // Don't actually close
}

// MARK: - Fullscreen Transitions

func windowDidEnterFullScreen(_ notification: Notification) {
guard let window = notification.object as? NSWindow,
MainWindowLayout.isPrimaryWindow(window)
else { return }
// macOS handles the autohiding titlebar + traffic lights natively in fullscreen.
// Keep titlebar transparent so the native overlay blends cleanly.
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
window.titlebarSeparatorStyle = .none
}

func windowDidExitFullScreen(_ notification: Notification) {
guard let window = notification.object as? NSWindow,
MainWindowLayout.isPrimaryWindow(window)
else { return }
// macOS may reset titlebar properties during the fullscreen transition.
// Re-apply windowed appearance after exiting.
MainWindowLayout.restoreWindowedAppearance(window)
}

// MARK: - Resize

func windowDidResize(_: Notification) {}
}

// MARK: UNUserNotificationCenterDelegate
Expand Down
89 changes: 89 additions & 0 deletions Sources/Kaset/Services/AudioFadeCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import Foundation
import WebKit

/// Central coordinator for player audio fading state, track crossfading, and background audio ducking.
@MainActor
final class AudioFadeCoordinator {
static let shared = AudioFadeCoordinator()

/// Current audio ducking state for background system events.
private(set) var isDucked: Bool = false
private var preDuckVolume: Double = 1.0

private init() {}

/// Performs a crossfade transition between currently playing track and next track.
func crossfadeTrackTransition(
webView: WKWebView?,
duration: TimeInterval = 1.5,
curve: AudioFaderService.FadeCurve = .logarithmic,
onNextTrackReady: @escaping @MainActor () -> Void
) {
guard let webView else {
onNextTrackReady()
return
}

// Step 1: Smoothly fade out current track
AudioFaderService.shared.fadeOut(
webView: webView,
duration: duration / 2.0,
curve: curve
) {
// Step 2: Trigger track change callback once volume is silenced
onNextTrackReady()

// Step 3: Smoothly fade back in for new track
AudioFaderService.shared.fadeIn(
webView: webView,
targetVolume: 1.0,
duration: duration / 2.0,
curve: curve
)
}
}

/// Smoothly ducks playback volume when system notifications or notifications play.
func duckAudio(
webView: WKWebView?,
targetDuckedVolume _: Double = 0.25,
duration: TimeInterval = 0.4
) {
guard let webView, !self.isDucked else { return }

self.isDucked = true
self.preDuckVolume = 1.0

let script = "if (document.querySelector('video')) { document.querySelector('video').volume; }"
webView.evaluateJavaScript(script) { result, _ in
if let currentVol = result as? Double {
Task { @MainActor in
AudioFadeCoordinator.shared.preDuckVolume = currentVol
AudioFaderService.shared.fadeOut(
webView: webView,
duration: duration,
curve: .linear
)
}
}
}
}

/// Restores pre-duck volume level after notification ends.
func restoreAudio(
webView: WKWebView?,
duration: TimeInterval = 0.5
) {
guard let webView, self.isDucked else { return }

self.isDucked = false
let restoredVolume = self.preDuckVolume

AudioFaderService.shared.fadeIn(
webView: webView,
targetVolume: restoredVolume,
duration: duration,
curve: .logarithmic
)
}
}
20 changes: 20 additions & 0 deletions Sources/Kaset/Services/AudioFader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation
import WebKit

/// Audio fader service for smooth volume transitions in WKWebView playback.
@MainActor
final class AudioFader {
static let shared = AudioFader()

private init() {}

/// Fade out volume to zero over specified duration
func fadeOut(webView: WKWebView?, duration: TimeInterval, completion: @escaping @MainActor () -> Void) {
AudioFaderService.shared.fadeOut(webView: webView, duration: duration, curve: .logarithmic, completion: completion)
}

/// Fade in volume from zero to target over specified duration
func fadeIn(webView: WKWebView?, targetVolume: Double = 1.0, duration: TimeInterval, completion: (@MainActor () -> Void)? = nil) {
AudioFaderService.shared.fadeIn(webView: webView, targetVolume: targetVolume, duration: duration, curve: .logarithmic, completion: completion)
}
}
Loading
Loading