2026-03-28 13:49:47 -07:00
import SwiftUI
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// MARK: - P e r - V i e w I n d e p e n d e n t C o n f i g u r a t i o n
// / H o l d s t h e f u l l v i s u a l c o n f i g u r a t i o n f o r o n e v i s u a l i z e r c o n t e x t ( N o w P l a y i n g o r M i n i P l a y e r ) .
// / E a c h s t y l e s t o r e s i t s o w n p o i n t c o u n t a n d s e n s i t i v i t y s o s w i t c h i n g s t y l e s
// / n e v e r b l e e d s p a r a m e t e r s f r o m a d i f f e r e n t s t y l e .
struct ViewVisualizerConfig : Codable {
// MARK: S t y l e & C o l o r
var style : VisualizerSettings . Style = . wave
var colorMode : VisualizerSettings . ColorMode = . dynamic
var alpha : Double = 0.6
// C u s t o m c o l o r s t o r e d a s c o m p o n e n t s — C o l o r i s n o t C o d a b l e
var customColorR : Double = 1.0
var customColorG : Double = 0.176
var customColorB : Double = 0.333
// MARK: P e r - s t y l e p o i n t c o u n t s ( i n d e p e n d e n t — s w i t c h i n g s t y l e s u s e s i t s o w n l a s t v a l u e )
var wavePoints : Int = 9
var barPoints : Int = 12
var linePoints : Int = 10
var siriPoints : Int = 16
// MARK: P e r - s t y l e s e n s i t i v i t y
var waveSensitivity : Double = 0.9
var barSensitivity : Double = 1.0
var lineSensitivity : Double = 0.9
var siriSensitivity : Double = 1.0
// MARK: C o m p u t e d f r o m a c t i v e s t y l e
var numberOfPoints : Int {
get {
switch style {
case . wave : return wavePoints
case . bar : return barPoints
case . line : return linePoints
case . siriWave : return siriPoints
}
}
set {
switch style {
case . wave : wavePoints = newValue
case . bar : barPoints = newValue
case . line : linePoints = newValue
case . siriWave : siriPoints = newValue
}
}
}
var sensitivity : Double {
get {
switch style {
case . wave : return waveSensitivity
case . bar : return barSensitivity
case . line : return lineSensitivity
case . siriWave : return siriSensitivity
}
}
set {
switch style {
case . wave : waveSensitivity = newValue
case . bar : barSensitivity = newValue
case . line : lineSensitivity = newValue
case . siriWave : siriSensitivity = newValue
}
}
}
var customColor : Color {
Color ( red : customColorR , green : customColorG , blue : customColorB )
}
// MARK: D e f a u l t s
static var nowPlayingDefault : ViewVisualizerConfig {
var c = ViewVisualizerConfig ( )
c . wavePoints = 9 ; c . barPoints = 12 ; c . linePoints = 10 ; c . siriPoints = 16
return c
}
static var miniPlayerDefault : ViewVisualizerConfig {
var c = ViewVisualizerConfig ( )
c . style = . wave ; c . alpha = 0.5
c . wavePoints = 8 ; c . barPoints = 10 ; c . linePoints = 8 ; c . siriPoints = 12
return c
}
}
// MARK: - V i s u a l i z e r S e t t i n g s ( g l o b a l + p e r - v i e w c o n f i g s )
2026-03-28 13:49:47 -07:00
class VisualizerSettings : ObservableObject {
static let shared = VisualizerSettings ( )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// MARK: G l o b a l t o g g l e s
@ Published var enabled : Bool { didSet { save ( " vis_enabled " , enabled ) } }
2026-03-28 13:49:47 -07:00
@ Published var nowPlayingEnabled : Bool { didSet { save ( " vis_nowplaying " , nowPlayingEnabled ) } }
@ Published var miniPlayerEnabled : Bool { didSet { save ( " vis_miniplayer " , miniPlayerEnabled ) } }
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// MARK: P e r - v i e w i n d e p e n d e n t c o n f i g s
@ Published var nowPlaying : ViewVisualizerConfig { didSet { saveConfig ( " vis_np_config " , nowPlaying ) } }
@ Published var miniPlayer : ViewVisualizerConfig { didSet { saveConfig ( " vis_mini_config " , miniPlayer ) } }
// MARK: G l o b a l p h y s i c s ( s h a r e d b e t w e e n v i e w s )
@ Published var fps : Double { didSet { save ( " vis_fps " , fps ) } }
@ Published var realAudioAnalysis : Bool { didSet { save ( " vis_real_fft " , realAudioAnalysis ) } }
@ Published var dynamicGainEnabled : Bool { didSet { save ( " vis_dynamic_gain " , dynamicGainEnabled ) } }
@ Published var viscosity : Double { didSet { save ( " vis_viscosity " , viscosity ) } }
@ Published var frequencyCutoff : Int { didSet { save ( " vis_freq_cutoff " , frequencyCutoff ) } }
@ Published var baseMultiplier : Double { didSet { save ( " vis_base_mult " , baseMultiplier ) } }
// MARK: W a v e - s p e c i f i c g l o b a l
2026-03-28 13:49:47 -07:00
@ Published var waveStrokeThickness : Double { didSet { save ( " vis_wave_stroke " , waveStrokeThickness ) } }
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// MARK: B a r - s p e c i f i c g l o b a l
@ Published var barSpacing : Double { didSet { save ( " vis_bar_spacing " , barSpacing ) } }
@ Published var barCornerRadius : Double { didSet { save ( " vis_bar_radius " , barCornerRadius ) } }
// MARK: L i n e - s p e c i f i c g l o b a l
@ Published var lineThickness : Double { didSet { save ( " vis_line_thick " , lineThickness ) } }
// MARK: N o w P l a y i n g l a y o u t / d e p t h
@ Published var nowPlayingHeightPct : Double { didSet { save ( " vis_np_height " , nowPlayingHeightPct ) } }
@ Published var waveOffsetTop : Double { didSet { save ( " vis_wave_offset " , waveOffsetTop ) } }
@ Published var npAmplitude : Double { didSet { save ( " vis_np_amplitude " , npAmplitude ) } }
@ Published var npBaseLift : Double { didSet { save ( " vis_np_baselift " , npBaseLift ) } }
@ Published var depthOffset : Double { didSet { save ( " vis_depth_offset " , depthOffset ) } }
@ Published var depthOpacity : Double { didSet { save ( " vis_depth_opacity " , depthOpacity ) } }
@ Published var idleAmplitude : Double { didSet { save ( " vis_idle_amp " , idleAmplitude ) } }
// MARK: M i n i P l a y e r l a y o u t / d e p t h
@ Published var miniPlayerHeight : Double { didSet { save ( " vis_mini_height " , miniPlayerHeight ) } }
@ Published var miniOpacity : Double { didSet { save ( " vis_mini_opacity " , miniOpacity ) } }
@ Published var miniAmplitude : Double { didSet { save ( " vis_mini_amplitude " , miniAmplitude ) } }
@ Published var miniIdleAmplitude : Double { didSet { save ( " vis_mini_idle " , miniIdleAmplitude ) } }
@ Published var miniDepthOffset : Double { didSet { save ( " vis_mini_depth " , miniDepthOffset ) } }
@ Published var miniDepthOpacity : Double { didSet { save ( " vis_mini_depth_opacity " , miniDepthOpacity ) } }
enum Style : String , CaseIterable , Codable {
case wave = " Wave "
case bar = " Bar "
case line = " Line "
2026-04-09 23:11:40 -07:00
case siriWave = " Siri "
2026-03-28 13:49:47 -07:00
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
enum ColorMode : String , CaseIterable , Codable {
case dynamic = " Dynamic "
2026-03-28 13:49:47 -07:00
case albumArt = " Album Art "
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
case vibrant = " Vibrant "
case custom = " Custom "
2026-03-28 13:49:47 -07:00
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
2026-03-28 13:49:47 -07:00
private init ( ) {
let d = UserDefaults . standard
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
enabled = d . object ( forKey : " vis_enabled " ) as ? Bool ? ? true
2026-03-28 13:49:47 -07:00
nowPlayingEnabled = d . object ( forKey : " vis_nowplaying " ) as ? Bool ? ? true
miniPlayerEnabled = d . object ( forKey : " vis_miniplayer " ) as ? Bool ? ? true
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// P e r - v i e w c o n f i g s — l o a d f r o m J S O N o r u s e d e f a u l t s
let dec = JSONDecoder ( )
if let data = d . data ( forKey : " vis_np_config " ) ,
let cfg = try ? dec . decode ( ViewVisualizerConfig . self , from : data ) {
nowPlaying = cfg
} else {
nowPlaying = . nowPlayingDefault
}
if let data = d . data ( forKey : " vis_mini_config " ) ,
let cfg = try ? dec . decode ( ViewVisualizerConfig . self , from : data ) {
miniPlayer = cfg
} else {
miniPlayer = . miniPlayerDefault
}
// G l o b a l p h y s i c s
fps = { let v = d . double ( forKey : " vis_fps " ) ; return v > 0 ? v : 60.0 } ( )
realAudioAnalysis = d . object ( forKey : " vis_real_fft " ) as ? Bool ? ? true
2026-04-04 23:17:47 -07:00
dynamicGainEnabled = d . object ( forKey : " vis_dynamic_gain " ) as ? Bool ? ? true
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
viscosity = { let v = d . double ( forKey : " vis_viscosity " ) ; return v > 0 ? v : 0.25 } ( )
frequencyCutoff = { let v = d . integer ( forKey : " vis_freq_cutoff " ) ; return v > 0 ? v : 80 } ( )
baseMultiplier = { let v = d . double ( forKey : " vis_base_mult " ) ; return v > 0 ? v : 25.0 } ( )
// S t y l e - s p e c i f i c g l o b a l s
2026-03-28 13:49:47 -07:00
waveStrokeThickness = { let v = d . double ( forKey : " vis_wave_stroke " ) ; return v > 0 ? v : 1.5 } ( )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
barSpacing = { let v = d . double ( forKey : " vis_bar_spacing " ) ; return v > 0 ? v : 5.0 } ( )
barCornerRadius = d . object ( forKey : " vis_bar_radius " ) as ? Double ? ? 0.0
lineThickness = { let v = d . double ( forKey : " vis_line_thick " ) ; return v > 0 ? v : 5.0 } ( )
// N o w P l a y i n g l a y o u t
nowPlayingHeightPct = { let v = d . double ( forKey : " vis_np_height " ) ; return v > 0 ? v : 0.50 } ( )
waveOffsetTop = d . object ( forKey : " vis_wave_offset " ) as ? Double ? ? 0.0
npAmplitude = { let v = d . double ( forKey : " vis_np_amplitude " ) ; return v > 0 ? v : 0.45 } ( )
npBaseLift = d . object ( forKey : " vis_np_baselift " ) as ? Double ? ? 130.0
// F i x : u s e o b j e c t ( f o r K e y : ) ? ? d e f a u l t s o s e t t i n g v a l u e t o 0 i s p e r s i s t e d c o r r e c t l y
depthOffset = d . object ( forKey : " vis_depth_offset " ) as ? Double ? ? 15.0
depthOpacity = d . object ( forKey : " vis_depth_opacity " ) as ? Double ? ? 0.2
idleAmplitude = { let v = d . double ( forKey : " vis_idle_amp " ) ; return v > 0 ? v : 0.03 } ( )
// M i n i P l a y e r l a y o u t
miniPlayerHeight = { let v = d . double ( forKey : " vis_mini_height " ) ; return v > 0 ? v : 48.0 } ( )
miniOpacity = d . object ( forKey : " vis_mini_opacity " ) as ? Double ? ? 0.5
miniAmplitude = { let v = d . double ( forKey : " vis_mini_amplitude " ) ; return v > 0 ? v : 0.7 } ( )
miniIdleAmplitude = { let v = d . double ( forKey : " vis_mini_idle " ) ; return v > 0 ? v : 0.03 } ( )
miniDepthOffset = d . object ( forKey : " vis_mini_depth " ) as ? Double ? ? 8.0
miniDepthOpacity = d . object ( forKey : " vis_mini_depth_opacity " ) as ? Double ? ? 0.2
2026-03-28 13:49:47 -07:00
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
2026-03-28 13:49:47 -07:00
var effectiveFPS : Double {
ProcessInfo . processInfo . isLowPowerModeEnabled ? min ( fps , 24 ) : fps
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// MARK: - P e r s i s t e n c e
2026-03-28 13:49:47 -07:00
private func save ( _ key : String , _ value : Any ) {
pendingSaves [ key ] = value
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
scheduleFlush ( )
}
private func saveConfig ( _ key : String , _ config : ViewVisualizerConfig ) {
if let data = try ? JSONEncoder ( ) . encode ( config ) {
pendingSaves [ key ] = data
scheduleFlush ( )
}
}
overhaul
AUDIT-036 — Slider/button fixes (direct Liquid Glass cause)
scheduleFlush() now runs Task { @MainActor } instead of bare Task. The
pendingSaves dictionary is now only ever read/written on the main
thread. Before this fix, a UserDefaults write could race with a slider
didSet, causing values to snap back or write the wrong value — which
is exactly why buttons were switching state unexpectedly.
AUDIT-034 — 60fps idle Canvas (direct Liquid Glass cause)
TimelineView now uses isRenderingActive ? settings.effectiveFPS : 2.0.
When paused or not visible, the Canvas drops from 60fps to 2fps. This
stops the continuous GPU wakeups that were fighting Liquid Glass
gesture tracking, which is why sliders needed multiple attempts.
AUDIT-001 — FFT real-time heap allocation
processFFT no longer allocates any heap memory. The Hann window is
computed once in init(). All four scratch buffers (fftWindow,
fftWindowed, fftRealp/fftImagp, fftMagnitudes) are pre-allocated and
reused every render callback — zero allocations on the real-time audio
thread.
AUDIT-002 — WatchOfflineStore data race
taskToSongId and pendingSongs now protected by a dedicated serial
storeQueue. URLSession delegate reads and main thread writes are
serialised.
AUDIT-019 — URLSession per AsyncCoverArt render
CompanionAPIService() no longer instantiated per render. Companion
cover art URLs now built directly from
CompanionSettings.shared.baseURL — no URLSession created.
AUDIT-020 — Synchronous disk read on main thread
CachedImageLoader now uses memoryOnlyImage (sync, no I/O) for the
first check, then cachedImageAsync (disk read on ioQueue) for the
second. Main thread never blocks on disk I/O.
AUDIT-033 — Lost star/unstar actions offline
Star/unstar now routes through OptimisticActionQueue — actions survive
Tailscale reconnection and are retried automatically.
AUDIT-035 — OptimisticActionQueue flush race
flush() Task is now @MainActor — pendingActions only ever touched on
main thread, no more race between rapid taps and in-flight flushes.
AUDIT-038 — O(n²) deduplication
deduplicateAlbums now O(n) using a frequency dictionary. For 843
albums: ~7.1M string comparisons/second during playback → ~1,700.
AUDIT-026, AUDIT-015 — Duplicate setResourceValue removed, cacheSize
now uses totalSize directly
2026-04-11 11:17:40 -07:00
// D e b o u n c e U s e r D e f a u l t s w r i t e s t o 5 0 0 m s s o r a p i d s l i d e r d r a g s d o n ' t
// f l o o d t h e d e f a u l t s s y s t e m . T h e T a s k i s e x p l i c i t l y @ M a i n A c t o r s o
// p e n d i n g S a v e s i s o n l y e v e r r e a d / w r i t t e n o n t h e m a i n t h r e a d — n o d a t a r a c e .
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
private func scheduleFlush ( ) {
2026-03-28 13:49:47 -07:00
saveTask ? . cancel ( )
overhaul
AUDIT-036 — Slider/button fixes (direct Liquid Glass cause)
scheduleFlush() now runs Task { @MainActor } instead of bare Task. The
pendingSaves dictionary is now only ever read/written on the main
thread. Before this fix, a UserDefaults write could race with a slider
didSet, causing values to snap back or write the wrong value — which
is exactly why buttons were switching state unexpectedly.
AUDIT-034 — 60fps idle Canvas (direct Liquid Glass cause)
TimelineView now uses isRenderingActive ? settings.effectiveFPS : 2.0.
When paused or not visible, the Canvas drops from 60fps to 2fps. This
stops the continuous GPU wakeups that were fighting Liquid Glass
gesture tracking, which is why sliders needed multiple attempts.
AUDIT-001 — FFT real-time heap allocation
processFFT no longer allocates any heap memory. The Hann window is
computed once in init(). All four scratch buffers (fftWindow,
fftWindowed, fftRealp/fftImagp, fftMagnitudes) are pre-allocated and
reused every render callback — zero allocations on the real-time audio
thread.
AUDIT-002 — WatchOfflineStore data race
taskToSongId and pendingSongs now protected by a dedicated serial
storeQueue. URLSession delegate reads and main thread writes are
serialised.
AUDIT-019 — URLSession per AsyncCoverArt render
CompanionAPIService() no longer instantiated per render. Companion
cover art URLs now built directly from
CompanionSettings.shared.baseURL — no URLSession created.
AUDIT-020 — Synchronous disk read on main thread
CachedImageLoader now uses memoryOnlyImage (sync, no I/O) for the
first check, then cachedImageAsync (disk read on ioQueue) for the
second. Main thread never blocks on disk I/O.
AUDIT-033 — Lost star/unstar actions offline
Star/unstar now routes through OptimisticActionQueue — actions survive
Tailscale reconnection and are retried automatically.
AUDIT-035 — OptimisticActionQueue flush race
flush() Task is now @MainActor — pendingActions only ever touched on
main thread, no more race between rapid taps and in-flight flushes.
AUDIT-038 — O(n²) deduplication
deduplicateAlbums now O(n) using a frequency dictionary. For 843
albums: ~7.1M string comparisons/second during playback → ~1,700.
AUDIT-026, AUDIT-015 — Duplicate setResourceValue removed, cacheSize
now uses totalSize directly
2026-04-11 11:17:40 -07:00
saveTask = Task { @ MainActor [ weak self ] in
2026-03-28 13:49:47 -07:00
try ? await Task . sleep ( nanoseconds : 500_000_000 )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
guard ! Task . isCancelled , let self else { return }
for ( k , v ) in self . pendingSaves {
UserDefaults . standard . set ( v , forKey : k )
2026-03-28 13:49:47 -07:00
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
self . pendingSaves . removeAll ( )
2026-03-28 13:49:47 -07:00
}
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
2026-03-28 13:49:47 -07:00
private var pendingSaves : [ String : Any ] = [ : ]
private var saveTask : Task < Void , Never > ?
}
2026-04-04 06:58:58 -07:00
// MARK: - W a v e S t a t e C a c h e ( s h a r e d b e t w e e n m i n i p l a y e r a n d D I f o r m o r p h c o n t i n u i t y )
// / S i n g l e s o u r c e o f t r u t h f o r c o m p a c t v i s u a l i z e r l e v e l s .
// / B o t h M i n i P l a y e r B a r a n d D y n a m i c I s l a n d V i e w s e e d f r o m h e r e o n a p p e a r ,
// / s o t h e w a v e d o e s n ' t r e s e t t o i d l e w h e n m a t c h e d G e o m e t r y E f f e c t t r a n s i t i o n s b e t w e e n t h e m .
final class WaveStateCache {
static let shared = WaveStateCache ( )
var compactLevels : [ Float ] = [ ]
private init ( ) { }
}
2026-04-04 23:17:47 -07:00
// MARK: - L e v e l B o x
2026-04-05 08:51:26 -07:00
// / P e r - i n s t a n c e m u t a b l e r e n d e r s t a t e . Z e r o @ P u b l i s h e d p r o p e r t i e s → n o o b s e r v a t i o n w a r n i n g s .
2026-04-10 16:25:49 -07:00
// / A l l w o r k i n g b u f f e r s p r e - a l l o c a t e d h e r e s o t h e 6 0 f p s r e n d e r l o o p d o e s z e r o h e a p a l l o c a t i o n .
2026-04-04 23:17:47 -07:00
fileprivate final class VisualizerLevelBox : ObservableObject {
2026-04-10 16:25:49 -07:00
var displayLevels : [ Float ] = [ ]
var targetLevels : [ Float ] = [ ] // s c r a t c h b u f f e r — p r e - a l l o c a t e d , m u t a t e d i n - p l a c e e a c h f r a m e
var idleLevels : [ Float ] = [ ] // f l a t i d l e w a v e — p r e - a l l o c a t e d , u p d a t e d o n l y w h e n c o u n t c h a n g e s
var peakFollower : Float = 0.01
2026-04-04 23:17:47 -07:00
var levelHistoryBuf : [ [ Float ] ] = [ ]
var historyWriteIdx : Int = 0
var wobblePhaseOffset : Double = 0
2026-04-10 16:25:49 -07:00
var lastTickTime : CFTimeInterval = 0
2026-04-10 19:05:45 -07:00
var resumeTickCount : Int = 0 // d e b u g : c o u n t s f i r s t N t i c k s a f t e r r e s u m e
2026-04-10 16:25:49 -07:00
// / R e s i z e a l l p e r - f r a m e b u f f e r s w h e n p o i n t c o u n t c h a n g e s ( r a r e — s e t t i n g s s l i d e r ) .
func resizeIfNeeded ( count : Int , idleAmplitude : Float ) {
guard count > 0 else { return }
if targetLevels . count != count {
targetLevels = [ Float ] ( repeating : 0 , count : count )
displayLevels = [ Float ] ( repeating : idleAmplitude , count : count )
idleLevels = [ Float ] ( repeating : idleAmplitude , count : count )
// P r e - f i l l h i s t o r y r i n g w i t h i d l e s o d e p t h g h o s t d o e s n ' t s t a r t b l a n k
levelHistoryBuf = [ [ Float ] ] (
repeating : [ Float ] ( repeating : idleAmplitude , count : count ) ,
count : MitsuhaVisualizerView . historySize
)
historyWriteIdx = 0
} else if idleLevels . first != idleAmplitude {
for i in 0. . < count { idleLevels [ i ] = idleAmplitude }
}
}
2026-04-04 23:17:47 -07:00
}
2026-03-28 13:49:47 -07:00
// MARK: - M a i n V i s u a l i z e r V i e w
struct MitsuhaVisualizerView : View {
var previewLevels : [ Float ] ? = nil
let isPlaying : Bool
2026-04-05 08:33:00 -07:00
var isSongLoaded : Bool = true
2026-04-11 19:01:10 -07:00
var songId : String ? = nil // u s e d t o r e s e t p e a k F o l l o w e r o n s o n g c h a n g e
2026-03-28 13:49:47 -07:00
let accentColor : Color
var compact : Bool = false
2026-04-10 13:16:12 -07:00
var isVisible : Bool = true
2026-04-04 23:17:47 -07:00
overhaul
AUDIT-036 — Slider/button fixes (direct Liquid Glass cause)
scheduleFlush() now runs Task { @MainActor } instead of bare Task. The
pendingSaves dictionary is now only ever read/written on the main
thread. Before this fix, a UserDefaults write could race with a slider
didSet, causing values to snap back or write the wrong value — which
is exactly why buttons were switching state unexpectedly.
AUDIT-034 — 60fps idle Canvas (direct Liquid Glass cause)
TimelineView now uses isRenderingActive ? settings.effectiveFPS : 2.0.
When paused or not visible, the Canvas drops from 60fps to 2fps. This
stops the continuous GPU wakeups that were fighting Liquid Glass
gesture tracking, which is why sliders needed multiple attempts.
AUDIT-001 — FFT real-time heap allocation
processFFT no longer allocates any heap memory. The Hann window is
computed once in init(). All four scratch buffers (fftWindow,
fftWindowed, fftRealp/fftImagp, fftMagnitudes) are pre-allocated and
reused every render callback — zero allocations on the real-time audio
thread.
AUDIT-002 — WatchOfflineStore data race
taskToSongId and pendingSongs now protected by a dedicated serial
storeQueue. URLSession delegate reads and main thread writes are
serialised.
AUDIT-019 — URLSession per AsyncCoverArt render
CompanionAPIService() no longer instantiated per render. Companion
cover art URLs now built directly from
CompanionSettings.shared.baseURL — no URLSession created.
AUDIT-020 — Synchronous disk read on main thread
CachedImageLoader now uses memoryOnlyImage (sync, no I/O) for the
first check, then cachedImageAsync (disk read on ioQueue) for the
second. Main thread never blocks on disk I/O.
AUDIT-033 — Lost star/unstar actions offline
Star/unstar now routes through OptimisticActionQueue — actions survive
Tailscale reconnection and are retried automatically.
AUDIT-035 — OptimisticActionQueue flush race
flush() Task is now @MainActor — pendingActions only ever touched on
main thread, no more race between rapid taps and in-flight flushes.
AUDIT-038 — O(n²) deduplication
deduplicateAlbums now O(n) using a frequency dictionary. For 843
albums: ~7.1M string comparisons/second during playback → ~1,700.
AUDIT-026, AUDIT-015 — Duplicate setResourceValue removed, cacheSize
now uses totalSize directly
2026-04-11 11:17:40 -07:00
// O b s e r v e s e t t i n g s o n l y f o r t h e e n a b l e d g a t e a n d c o n f i g v a l u e s .
// T h e C a n v a s i t s e l f d o e s N O T o b s e r v e s e t t i n g s — w e p a s s t h e v a l u e s i t
// n e e d s a s l o c a l c o n s t a n t s c a p t u r e d a t b o d y e v a l u a t i o n t i m e . T h i s m e a n s
// a s l i d e r d r a g i n V i s u a l i z e r S e t t i n g s V i e w d o e s N O T i n v a l i d a t e t h e C a n v a s
// o n e v e r y c h a n g e — o n l y t h e o u t e r v i e w r e - e v a l u a t e s , a n d t h e C a n v a s
// o n l y r e - e v a l u a t e s w h e n i s P l a y i n g / i s V i s i b l e / i s A p p A c t i v e c h a n g e s .
2026-03-28 13:49:47 -07:00
@ ObservedObject var settings = VisualizerSettings . shared
@ ObservedObject var albumColors = AlbumColorExtractor . shared
2026-04-05 12:05:20 -07:00
@ StateObject private var box = VisualizerLevelBox ( )
2026-04-10 13:16:12 -07:00
@ State private var isAppActive = true
2026-04-10 16:25:49 -07:00
static let historySize = 16
2026-04-04 23:17:47 -07:00
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// / A c t i v e v i e w c o n f i g — m i n i p l a y e r u s e s i t s o w n i n d e p e n d e n t c o n f i g
private var config : ViewVisualizerConfig {
compact ? settings . miniPlayer : settings . nowPlaying
}
2026-04-10 13:16:12 -07:00
private var isRenderingActive : Bool {
settings . enabled && isPlaying && isAppActive && isVisible
}
2026-03-28 13:49:47 -07:00
var body : some View {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
Group {
2026-04-04 07:05:25 -07:00
if settings . enabled {
overhaul
AUDIT-036 — Slider/button fixes (direct Liquid Glass cause)
scheduleFlush() now runs Task { @MainActor } instead of bare Task. The
pendingSaves dictionary is now only ever read/written on the main
thread. Before this fix, a UserDefaults write could race with a slider
didSet, causing values to snap back or write the wrong value — which
is exactly why buttons were switching state unexpectedly.
AUDIT-034 — 60fps idle Canvas (direct Liquid Glass cause)
TimelineView now uses isRenderingActive ? settings.effectiveFPS : 2.0.
When paused or not visible, the Canvas drops from 60fps to 2fps. This
stops the continuous GPU wakeups that were fighting Liquid Glass
gesture tracking, which is why sliders needed multiple attempts.
AUDIT-001 — FFT real-time heap allocation
processFFT no longer allocates any heap memory. The Hann window is
computed once in init(). All four scratch buffers (fftWindow,
fftWindowed, fftRealp/fftImagp, fftMagnitudes) are pre-allocated and
reused every render callback — zero allocations on the real-time audio
thread.
AUDIT-002 — WatchOfflineStore data race
taskToSongId and pendingSongs now protected by a dedicated serial
storeQueue. URLSession delegate reads and main thread writes are
serialised.
AUDIT-019 — URLSession per AsyncCoverArt render
CompanionAPIService() no longer instantiated per render. Companion
cover art URLs now built directly from
CompanionSettings.shared.baseURL — no URLSession created.
AUDIT-020 — Synchronous disk read on main thread
CachedImageLoader now uses memoryOnlyImage (sync, no I/O) for the
first check, then cachedImageAsync (disk read on ioQueue) for the
second. Main thread never blocks on disk I/O.
AUDIT-033 — Lost star/unstar actions offline
Star/unstar now routes through OptimisticActionQueue — actions survive
Tailscale reconnection and are retried automatically.
AUDIT-035 — OptimisticActionQueue flush race
flush() Task is now @MainActor — pendingActions only ever touched on
main thread, no more race between rapid taps and in-flight flushes.
AUDIT-038 — O(n²) deduplication
deduplicateAlbums now O(n) using a frequency dictionary. For 843
albums: ~7.1M string comparisons/second during playback → ~1,700.
AUDIT-026, AUDIT-015 — Duplicate setResourceValue removed, cacheSize
now uses totalSize directly
2026-04-11 11:17:40 -07:00
// A d a p t i v e F P S : f u l l r a t e w h e n r e n d e r i n g , 2 f p s w h e n i d l e / p a u s e d .
// T h i s p r e v e n t s t h e 6 0 f p s G P U w a k e u p t h a t d r a i n s b a t t e r y d u r i n g p a u s e
// a n d s t o p s t h e r e n d e r l o o p f r o m f i g h t i n g L i q u i d G l a s s g e s t u r e t r a c k i n g .
let targetFPS = isRenderingActive ? settings . effectiveFPS : 2.0
let tickInterval = 1.0 / max ( targetFPS , 1.0 )
2026-04-11 19:01:10 -07:00
// U s e a s t a b l e s t a r t d a t e s o t h e p e r i o d i c s c h e d u l e o r i g i n n e v e r
// r e s e t s . P r e v i o u s l y , . n o w w a s p a s s e d e v e r y t i m e t a r g e t F P S c h a n g e d
// ( e . g . o n a r a d i o b u f f e r h i c c u p ) , c a u s i n g t h e n e x t t i c k t o b e
// d e f e r r e d b y a f u l l i n t e r v a l — t h e v i s i b l e " s t a l l " i n t h e w a v e .
TimelineView ( . periodic ( from : . distantPast , by : tickInterval ) ) { timeline in
2026-04-10 19:20:27 -07:00
let tickDate = timeline . date
Canvas { context , size in
_ = tickDate
box . resizeIfNeeded ( count : config . numberOfPoints ,
idleAmplitude : Float ( settings . idleAmplitude ) )
guard box . displayLevels . count >= 2 else { return }
if isRenderingActive {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
let t = CACurrentMediaTime ( )
let rawLevels = previewLevels ? ? AudioPlayer . shared . currentLevels ( )
2026-04-10 19:05:45 -07:00
if box . resumeTickCount < 3 {
box . resumeTickCount += 1
DebugLogger . shared . log (
" TL tick # \( box . resumeTickCount ) : " +
" rawCount= \( rawLevels . count ) " +
" rawMax= \( String ( format : " %.3f " , rawLevels . max ( ) ? ? 0 ) ) " +
" lastTick= \( String ( format : " %.1f " , box . lastTickTime ) ) " ,
category : " VisDebug " , level : . debug )
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
updateDisplayLevels ( newRawLevels : rawLevels , t : t )
updateWobblePhase ( t : t )
switch config . style {
case . wave : drawWave ( ctx : context , size : size , levels : box . displayLevels , continuousTime : box . wobblePhaseOffset )
case . bar : drawBars ( ctx : context , size : size , levels : box . displayLevels )
case . line : drawLine ( ctx : context , size : size , levels : box . displayLevels )
case . siriWave : drawSiriWave ( ctx : context , size : size , levels : box . displayLevels , continuousTime : box . wobblePhaseOffset )
}
2026-04-10 19:20:27 -07:00
} else {
// I d l e / p a u s e d : d r a w l a s t k n o w n s t a t e w i t h o u t u p d a t i n g l e v e l s
drawIdleState ( ctx : context , size : size )
2026-04-10 16:25:49 -07:00
}
}
2026-04-09 23:11:40 -07:00
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
}
}
. opacity ( isPlaying ? 1.0 : ( isSongLoaded ? 0.35 : 1.0 ) )
. animation ( isSongLoaded ? . easeInOut ( duration : 0.6 ) : nil , value : isPlaying )
. onAppear {
box . resizeIfNeeded ( count : config . numberOfPoints ,
idleAmplitude : Float ( settings . idleAmplitude ) )
if compact , ! WaveStateCache . shared . compactLevels . isEmpty {
let cached = WaveStateCache . shared . compactLevels
let count = box . displayLevels . count
for i in 0. . < min ( cached . count , count ) {
box . displayLevels [ i ] = cached [ i ]
2026-04-10 13:16:12 -07:00
}
2026-03-28 13:49:47 -07:00
}
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
. onChange ( of : isPlaying ) { _ , playing in
if ! playing {
box . levelHistoryBuf . removeAll ( keepingCapacity : true )
box . historyWriteIdx = 0
box . peakFollower = 0.01
2026-04-11 19:01:10 -07:00
box . lastTickTime = 0
2026-04-10 19:05:45 -07:00
DebugLogger . shared . log (
" PAUSE → levelHistoryBuf cleared, lastTickTime reset " +
" [targets: \( box . targetLevels . count ) display: \( box . displayLevels . count ) ] " ,
category : " VisDebug " , level : . info )
} else {
2026-04-11 19:01:10 -07:00
box . resumeTickCount = 0
2026-04-10 19:05:45 -07:00
DebugLogger . shared . log (
" RESUME → isRenderingActive= \( isRenderingActive ) " +
" isAppActive= \( isAppActive ) isVisible= \( isVisible ) " +
" [targets: \( box . targetLevels . count ) display: \( box . displayLevels . count ) " +
" history: \( box . levelHistoryBuf . count ) ] " ,
category : " VisDebug " , level : . info )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
}
}
2026-04-11 19:01:10 -07:00
// R e s e t p e a k F o l l o w e r o n s o n g c h a n g e s o a s p i k e f r o m t h e o u t g o i n g
// s i m u l a t i o n d o e s n ' t " r a i s e " t h e w a v e a t t h e s t a r t o f t h e n e x t s o n g .
. onChange ( of : songId ) { _ , _ in
box . peakFollower = 0.01
box . levelHistoryBuf . removeAll ( keepingCapacity : true )
box . historyWriteIdx = 0
box . lastTickTime = 0
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
. onReceive ( NotificationCenter . default . publisher ( for : UIApplication . didBecomeActiveNotification ) ) { _ in
isAppActive = true
box . lastTickTime = 0
}
. onReceive ( NotificationCenter . default . publisher ( for : UIApplication . willResignActiveNotification ) ) { _ in
isAppActive = false
box . levelHistoryBuf . removeAll ( keepingCapacity : true )
box . historyWriteIdx = 0
}
2026-03-28 13:49:47 -07:00
}
2026-04-10 13:16:12 -07:00
// MARK: - I d l e S t a t e
private func drawIdleState ( ctx : GraphicsContext , size : CGSize ) {
2026-04-10 16:25:49 -07:00
guard box . idleLevels . count >= 2 else { return }
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
switch config . style {
2026-04-10 16:25:49 -07:00
case . wave : drawWave ( ctx : ctx , size : size , levels : box . idleLevels , continuousTime : 0 )
case . bar : drawBars ( ctx : ctx , size : size , levels : box . idleLevels )
case . line : drawLine ( ctx : ctx , size : size , levels : box . idleLevels )
case . siriWave : drawSiriWave ( ctx : ctx , size : size , levels : box . idleLevels , continuousTime : 0 )
2026-04-10 13:16:12 -07:00
}
}
2026-03-28 13:49:47 -07:00
2026-04-05 08:51:26 -07:00
// MARK: - W o b b l e P h a s e
2026-04-04 23:17:47 -07:00
2026-04-05 22:47:57 -07:00
private func updateWobblePhase ( t : Double ) {
2026-04-05 08:51:26 -07:00
if box . lastTickTime > 0 {
2026-04-05 22:47:57 -07:00
let delta = t - box . lastTickTime
2026-04-04 23:17:47 -07:00
box . wobblePhaseOffset += min ( delta , 0.1 )
2026-03-28 13:49:47 -07:00
}
2026-04-05 22:47:57 -07:00
box . lastTickTime = t
2026-03-28 13:49:47 -07:00
}
// MARK: - T e m p o r a l S m o o t h i n g & L o g B i n n i n g
2026-04-05 08:33:00 -07:00
@ discardableResult
2026-04-10 07:00:30 -07:00
private func updateDisplayLevels ( newRawLevels : [ Float ] , t : Double ) -> Bool {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
let count = config . numberOfPoints
2026-04-10 16:25:49 -07:00
guard count > 0 , ! newRawLevels . isEmpty ,
2026-04-10 19:05:45 -07:00
box . targetLevels . count = = count else {
DebugLogger . shared . log (
" updateDisplayLevels GUARD FAILED: " +
" count= \( count ) rawEmpty= \( newRawLevels . isEmpty ) " +
" targetCount= \( box . targetLevels . count ) " ,
category : " VisDebug " , level : . warning )
return false
}
2026-04-10 07:00:30 -07:00
2026-04-10 16:25:49 -07:00
let dt = Float ( box . lastTickTime > 0 ? min ( t - box . lastTickTime , 0.1 ) : 1.0 / 60.0 )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
let sens = Float ( config . sensitivity )
2026-03-28 13:49:47 -07:00
let isPreProcessed = AudioPlayer . shared . isUsingOfflineVis
2026-04-10 07:00:30 -07:00
2026-04-10 16:25:49 -07:00
// ─ ─ W r i t e d i r e c t l y i n t o p r e - a l l o c a t e d b o x . t a r g e t L e v e l s ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
2026-03-28 13:49:47 -07:00
if isPreProcessed {
if newRawLevels . count = = count {
2026-04-10 16:25:49 -07:00
// H o t p a t h : i d e n t i c a l s i z e — s i n g l e i n - p l a c e p a s s , z e r o a l l o c a t i o n
for i in 0. . < count {
box . targetLevels [ i ] = min ( 1.0 , newRawLevels [ i ] * sens )
}
2026-03-28 13:49:47 -07:00
} else {
2026-04-10 16:25:49 -07:00
// R e s a m p l e v i a l i n e a r i n t e r p o l a t i o n d i r e c t l y i n t o b o x . t a r g e t L e v e l s
let srcCount = Float ( newRawLevels . count - 1 )
let dstCount = Float ( max ( count - 1 , 1 ) )
2026-04-10 13:16:12 -07:00
for i in 0. . < count {
2026-04-10 16:25:49 -07:00
let srcPos = Float ( i ) / dstCount * srcCount
let lo = Int ( srcPos )
let hi = min ( lo + 1 , newRawLevels . count - 1 )
let frac = srcPos - Float ( lo )
box . targetLevels [ i ] = min ( 1.0 ,
( newRawLevels [ lo ] * ( 1.0 - frac ) + newRawLevels [ hi ] * frac ) * sens )
2026-03-28 13:49:47 -07:00
}
}
} else {
2026-04-10 16:25:49 -07:00
// R a w F F T — l o g b i n n i n g d i r e c t l y i n t o b o x . t a r g e t L e v e l s
2026-03-28 13:49:47 -07:00
let maxUsefulBin = min ( newRawLevels . count - 1 , settings . frequencyCutoff )
2026-04-10 16:25:49 -07:00
let mult = Float ( settings . baseMultiplier )
2026-03-28 13:49:47 -07:00
for i in 0. . < count {
2026-04-10 16:25:49 -07:00
let ni = Float ( i + 1 ) / Float ( count )
let centerBin = log10 ( ni * 9.0 + 1.0 ) * Float ( maxUsefulBin )
let binWidth = max ( 1.0 , Float ( maxUsefulBin ) / Float ( count ) )
let startBin = max ( 1 , Int ( centerBin - binWidth * 0.5 ) )
let endBin = min ( maxUsefulBin , Int ( centerBin + binWidth * 0.5 ) )
2026-03-28 13:49:47 -07:00
var sum : Float = 0
2026-04-10 16:25:49 -07:00
var n = 0
2026-03-28 13:49:47 -07:00
for j in startBin . . . endBin where j < newRawLevels . count {
2026-04-10 16:25:49 -07:00
sum += newRawLevels [ j ] ; n += 1
2026-03-28 13:49:47 -07:00
}
2026-04-10 16:25:49 -07:00
box . targetLevels [ i ] = min ( 1.0 , ( n > 0 ? sum / Float ( n ) : 0 ) * mult * sens )
2026-03-28 13:49:47 -07:00
}
}
2026-04-10 13:16:12 -07:00
2026-04-10 16:25:49 -07:00
// ─ ─ D y n a m i c G a i n / P e a k F o l l o w e r ( i n - p l a c e o n b o x . t a r g e t L e v e l s ) ─ ─ ─ ─ ─ ─
2026-04-05 22:47:57 -07:00
let smoothFactor = min ( Float ( settings . viscosity ) * 60.0 * dt , 1.0 )
2026-04-10 16:25:49 -07:00
let frameMax = box . targetLevels . max ( ) ? ? 0.0
2026-04-04 23:17:47 -07:00
if settings . dynamicGainEnabled {
2026-04-09 23:39:52 -07:00
if frameMax > box . peakFollower {
2026-04-10 16:25:49 -07:00
box . peakFollower += ( frameMax - box . peakFollower ) * 0.3
2026-04-09 23:39:52 -07:00
} else {
2026-04-10 16:25:49 -07:00
box . peakFollower = max ( box . peakFollower * pow ( 0.5 , dt ) , 0.01 )
2026-04-09 23:39:52 -07:00
}
2026-04-10 16:25:49 -07:00
let normFactor = 1.0 / max ( box . peakFollower , 0.01 )
for i in 0. . < count {
box . targetLevels [ i ] = min ( box . targetLevels [ i ] * normFactor , 1.0 )
2026-04-10 13:16:12 -07:00
}
2026-04-09 23:39:52 -07:00
} else {
box . peakFollower = max ( box . peakFollower , frameMax )
2026-04-04 23:17:47 -07:00
}
2026-04-10 16:25:49 -07:00
// ─ ─ E x p o n e n t i a l s m o o t h i n t o d i s p l a y L e v e l s ( i n - p l a c e ) ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
for i in 0. . < count {
box . displayLevels [ i ] += ( box . targetLevels [ i ] - box . displayLevels [ i ] ) * smoothFactor
2026-03-28 13:49:47 -07:00
}
2026-04-04 23:17:47 -07:00
2026-04-10 16:25:49 -07:00
// ─ ─ R i n g b u f f e r : c o p y i n - p l a c e i n t o p r e - a l l o c a t e d h i s t o r y s l o t ─ ─ ─ ─ ─ ─ ─ ─
// P r e - a l l o c a t e d s l o t s m e a n n o C O W t r i g g e r o n a s s i g n m e n t .
2026-04-04 23:17:47 -07:00
if box . levelHistoryBuf . count < MitsuhaVisualizerView . historySize {
box . levelHistoryBuf . append ( box . displayLevels )
} else {
2026-04-10 16:25:49 -07:00
let idx = box . historyWriteIdx
if box . levelHistoryBuf [ idx ] . count = = count {
for i in 0. . < count {
box . levelHistoryBuf [ idx ] [ i ] = box . displayLevels [ i ]
}
} else {
box . levelHistoryBuf [ idx ] = box . displayLevels
}
2026-04-04 23:17:47 -07:00
box . historyWriteIdx = ( box . historyWriteIdx + 1 ) % MitsuhaVisualizerView . historySize
}
if compact { WaveStateCache . shared . compactLevels = box . displayLevels }
2026-04-05 08:33:00 -07:00
return true
2026-03-28 13:49:47 -07:00
}
// MARK: - C o l o r s
private var fillColors : [ Color ] {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
let a = config . alpha
switch config . colorMode {
case . dynamic : return [ accentColor . opacity ( a ) , accentColor . opacity ( a * 0.6 ) ]
2026-03-28 13:49:47 -07:00
case . albumArt : return [ albumColors . primaryColor . opacity ( a ) , albumColors . secondaryColor . opacity ( a * 0.7 ) ]
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
case . vibrant : return [ . pink . opacity ( a ) , . green . opacity ( a ) , . cyan . opacity ( a ) , . purple . opacity ( a ) , . white . opacity ( a * 0.5 ) , . pink . opacity ( a ) ]
case . custom : return [ config . customColor . opacity ( a ) , config . customColor . opacity ( a * 0.6 ) ]
2026-03-28 13:49:47 -07:00
}
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
2026-03-28 13:49:47 -07:00
private var strokeColor : Color {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
let a = min ( 1 , config . alpha + 0.3 )
switch config . colorMode {
case . dynamic : return accentColor . opacity ( a )
case . albumArt : return albumColors . primaryColor . opacity ( a )
case . vibrant : return Color . cyan . opacity ( a )
case . custom : return config . customColor . opacity ( a )
2026-03-28 13:49:47 -07:00
}
}
// MARK: - C a t m u l l - R o m S p l i n e ( t e n s i o n 0 . 3 = h e a v y l i q u i d s u r f a c e t e n s i o n )
// / T e n s i o n : 0 . 3 g i v e s r o u n d e d , r o l l i n g p e a k s . L o w e r = t i g h t e r . H i g h e r = b o u n c i e r .
private let curveTension : CGFloat = 0.3
// / C u r v e p a t h w i t h o u t i n i t i a l m o v e T o — u s e d f o r f i l l s h a p e s
private func smoothCurve ( _ points : [ CGPoint ] ) -> Path {
var p = Path ( )
guard points . count >= 2 else { return p }
for i in 0. . < ( points . count - 1 ) {
let p0 = i > 0 ? points [ i - 1 ] : points [ 0 ]
let p1 = points [ i ]
let p2 = points [ i + 1 ]
let p3 = i < points . count - 2 ? points [ i + 2 ] : p2
let cp1 = CGPoint (
x : p1 . x + ( p2 . x - p0 . x ) * curveTension ,
y : p1 . y + ( p2 . y - p0 . y ) * curveTension
)
let cp2 = CGPoint (
x : p2 . x - ( p3 . x - p1 . x ) * curveTension ,
y : p2 . y - ( p3 . y - p1 . y ) * curveTension
)
p . addCurve ( to : p2 , control1 : cp1 , control2 : cp2 )
}
return p
}
// / F u l l c u r v e p a t h w i t h m o v e T o — u s e d f o r s t r o k e s
private func strokeableCurve ( _ points : [ CGPoint ] ) -> Path {
var p = Path ( )
guard points . count >= 2 else { return p }
p . move ( to : points [ 0 ] )
for i in 0. . < ( points . count - 1 ) {
let p0 = i > 0 ? points [ i - 1 ] : points [ 0 ]
let p1 = points [ i ]
let p2 = points [ i + 1 ]
let p3 = i < points . count - 2 ? points [ i + 2 ] : p2
let cp1 = CGPoint (
x : p1 . x + ( p2 . x - p0 . x ) * curveTension ,
y : p1 . y + ( p2 . y - p0 . y ) * curveTension
)
let cp2 = CGPoint (
x : p2 . x - ( p3 . x - p1 . x ) * curveTension ,
y : p2 . y - ( p3 . y - p1 . y ) * curveTension
)
p . addCurve ( to : p2 , control1 : cp1 , control2 : cp2 )
}
return p
}
// MARK: - W a v e
2026-04-04 23:17:47 -07:00
private func drawWave ( ctx : GraphicsContext , size : CGSize , levels : [ Float ] , continuousTime : Double ) {
2026-03-28 13:49:47 -07:00
let w = size . width
let h = size . height
let idleVal = CGFloat ( compact ? settings . miniIdleAmplitude : settings . idleAmplitude )
guard levels . count >= 2 else { return }
2026-04-04 23:17:47 -07:00
let count = levels . count
let spacing = w / CGFloat ( count - 1 )
let now = continuousTime
2026-03-28 13:49:47 -07:00
let baseLift : CGFloat = compact ? 0 : CGFloat ( settings . npBaseLift )
let offset = compact ? 0 : CGFloat ( settings . waveOffsetTop )
let baseline = h - baseLift - offset
let ampScale : CGFloat = compact ? CGFloat ( settings . miniAmplitude ) : CGFloat ( settings . npAmplitude )
2026-04-04 23:17:47 -07:00
var points = levels . enumerated ( ) . map { i , lev -> CGPoint in
2026-03-28 13:49:47 -07:00
let x = CGFloat ( i ) * spacing
let baseAmp = CGFloat ( lev )
let organicWobble = CGFloat ( sin ( now * 3.0 + ( Double ( i ) * 0.8 ) ) * 0.03 )
2026-04-04 23:17:47 -07:00
let totalAmp = max ( idleVal , baseAmp + organicWobble )
2026-03-28 13:49:47 -07:00
return CGPoint ( x : x , y : baseline - ( totalAmp * h * ampScale ) )
}
2026-04-04 23:17:47 -07:00
// ─ ─ E n d p o i n t A n c h o r i n g ( i m p r o v e m e n t # 1 3 ) ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
// P i n f i r s t a n d l a s t p o i n t s e x a c t l y t o b a s e l i n e s o t h e w a v e c l e a n l y
// r i s e s f r o m a n d r e t u r n s t o t h e h o r i z o n — m a t c h i n g t h e o r i g i n a l t w e a k .
points [ 0 ] = CGPoint ( x : 0 , y : baseline )
points [ count - 1 ] = CGPoint ( x : w , y : baseline )
// ─ ─ L a y e r 1 : T e m p o r a l d e p t h g h o s t ( i m p r o v e m e n t # 1 1 ) ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
// U s e t h e o l d e s t r i n g - b u f f e r f r a m e ( ~ 2 5 0 m s a g o ) f o r t h e s h a d o w w a v e ,
// m a t c h i n g t h e o r i g i n a l ' s 0 . 2 5 s d i s p a t c h _ a f t e r d e l a y e x a c t l y .
2026-03-28 13:49:47 -07:00
let ctxDepthOffset = compact ? CGFloat ( settings . miniDepthOffset ) : CGFloat ( settings . depthOffset )
let ctxDepthOpacity = compact ? settings . miniDepthOpacity : settings . depthOpacity
2026-04-04 23:17:47 -07:00
let ghostLevels : [ Float ]
let histFull = box . levelHistoryBuf . count >= MitsuhaVisualizerView . historySize
if histFull {
ghostLevels = box . levelHistoryBuf [ box . historyWriteIdx ]
} else {
ghostLevels = box . levelHistoryBuf . first ? ? Array ( repeating : 0 , count : count )
}
let ghostCount = max ( ghostLevels . count , 2 )
var depthPts = ( 0. . < count ) . map { i -> CGPoint in
// S a m p l e t h e g h o s t f r a m e a t t h e s a m e n o r m a l i s e d p o s i t i o n
let srcF = Float ( i ) / Float ( count - 1 ) * Float ( ghostCount - 1 )
let lo = min ( Int ( srcF ) , ghostCount - 1 )
let hi = min ( lo + 1 , ghostCount - 1 )
let frac = CGFloat ( srcF - Float ( lo ) )
let ghostLev = CGFloat ( ghostLevels [ lo ] ) * ( 1 - frac ) + CGFloat ( ghostLevels [ hi ] ) * frac
let ghostAmp = max ( idleVal , ghostLev )
let x = CGFloat ( i ) * spacing
return CGPoint ( x : x , y : baseline - ( ghostAmp * h * ampScale ) + ctxDepthOffset + 5 )
2026-03-28 13:49:47 -07:00
}
2026-04-04 23:17:47 -07:00
// A n c h o r g h o s t e n d p o i n t s t o o
depthPts [ 0 ] = CGPoint ( x : 0 , y : baseline + ctxDepthOffset )
depthPts [ count - 1 ] = CGPoint ( x : w , y : baseline + ctxDepthOffset )
2026-03-28 13:49:47 -07:00
let depthCurve = smoothCurve ( depthPts )
var depthFill = Path ( )
depthFill . move ( to : CGPoint ( x : 0 , y : h ) )
depthFill . addLine ( to : depthPts [ 0 ] )
depthFill . addPath ( depthCurve )
depthFill . addLine ( to : CGPoint ( x : w , y : h ) )
depthFill . closeSubpath ( )
ctx . fill ( depthFill , with : . color ( strokeColor . opacity ( ctxDepthOpacity ) ) )
// L a y e r 2 : M a i n f i l l — g r a d i e n t f r o m w a v e c r e s t t o t r u e b o t t o m
let curve = smoothCurve ( points )
var fill = Path ( )
fill . move ( to : CGPoint ( x : 0 , y : h ) )
fill . addLine ( to : points [ 0 ] )
fill . addPath ( curve )
fill . addLine ( to : CGPoint ( x : w , y : h ) )
fill . closeSubpath ( )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
let isSiri = config . colorMode = = . vibrant
2026-03-28 13:49:47 -07:00
if isSiri {
ctx . fill ( fill , with : . linearGradient (
Gradient ( colors : fillColors ) ,
startPoint : CGPoint ( x : 0 , y : 0 ) ,
endPoint : CGPoint ( x : w , y : 0 )
) )
} else {
ctx . fill ( fill , with : . linearGradient (
Gradient ( colors : fillColors ) ,
startPoint : CGPoint ( x : w / 2 , y : baseline - h * 0.3 ) ,
endPoint : CGPoint ( x : w / 2 , y : h )
) )
}
// L a y e r 3 : S t r o k e l i n e
ctx . stroke ( strokeableCurve ( points ) , with : . color ( strokeColor ) , lineWidth : CGFloat ( settings . waveStrokeThickness ) )
}
// MARK: - B a r s
private func drawBars ( ctx : GraphicsContext , size : CGSize , levels : [ Float ] ) {
let w = size . width
let h = size . height
let count = levels . count
guard count > 0 else { return }
let baseLift : CGFloat = compact ? 0 : CGFloat ( settings . npBaseLift )
let offset = compact ? 0 : CGFloat ( settings . waveOffsetTop )
let baseline = h - baseLift - offset
let ampScale : CGFloat = compact ? CGFloat ( settings . miniAmplitude ) : CGFloat ( settings . npAmplitude )
let gap = CGFloat ( settings . barSpacing )
let cr = CGFloat ( settings . barCornerRadius )
let totalGapSpace = gap * CGFloat ( count - 1 )
let barW = max ( 1 , ( w - totalGapSpace ) / CGFloat ( count ) )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
let isSiri = config . colorMode = = . vibrant
2026-04-10 16:25:49 -07:00
// H o i s t f i l l C o l o r s o u t s i d e l o o p — w a s a l l o c a t i n g [ C o l o r ] c o u n t t i m e s p e r f r a m e
let colors = fillColors
2026-03-28 13:49:47 -07:00
for i in 0. . < count {
2026-04-10 16:25:49 -07:00
let amp = CGFloat ( levels [ i ] )
let barH = max ( compact ? 2 : 4 , amp * h * ampScale )
let x = CGFloat ( i ) * ( barW + gap )
2026-03-28 13:49:47 -07:00
let barTop = baseline - barH
2026-04-10 16:25:49 -07:00
let rect = CGRect ( x : x , y : barTop , width : barW , height : h - barTop )
let path = Path ( roundedRect : rect , cornerRadius : cr )
2026-03-28 13:49:47 -07:00
let startPoint = isSiri ? CGPoint ( x : 0 , y : 0 ) : CGPoint ( x : x , y : barTop )
2026-04-10 16:25:49 -07:00
let endPoint = isSiri ? CGPoint ( x : w , y : 0 ) : CGPoint ( x : x , y : h )
2026-03-28 13:49:47 -07:00
ctx . fill ( path , with : . linearGradient (
2026-04-10 16:25:49 -07:00
Gradient ( colors : colors ) ,
2026-03-28 13:49:47 -07:00
startPoint : startPoint ,
endPoint : endPoint
) )
}
}
// MARK: - L i n e
private func drawLine ( ctx : GraphicsContext , size : CGSize , levels : [ Float ] ) {
let w = size . width
let h = size . height
guard levels . count >= 2 else { return }
let spacing = w / CGFloat ( levels . count - 1 )
let thick = CGFloat ( settings . lineThickness )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
let isSiri = config . colorMode = = . vibrant
2026-03-28 13:49:47 -07:00
let baseLift : CGFloat = compact ? 0 : CGFloat ( settings . npBaseLift )
let offset = compact ? 0 : CGFloat ( settings . waveOffsetTop )
let centerY = compact ? h * 0.5 : h - baseLift - offset
let ampMult : CGFloat = compact ? CGFloat ( settings . miniAmplitude ) * 0.5 : CGFloat ( settings . npAmplitude ) * 0.55
let points = levels . enumerated ( ) . map { i , lev -> CGPoint in
let dir : CGFloat = i % 2 = = 0 ? - 1 : 1
let amp = CGFloat ( lev ) * ( h * ampMult )
return CGPoint ( x : CGFloat ( i ) * spacing , y : centerY + ( dir * amp ) )
}
let curve = strokeableCurve ( points )
let strokeStyle = StrokeStyle ( lineWidth : thick , lineCap : . round , lineJoin : . round )
// G l o w
ctx . stroke ( curve , with : . color ( strokeColor . opacity ( 0.3 ) ) , style : StrokeStyle ( lineWidth : thick + 6 , lineCap : . round , lineJoin : . round ) )
// C o r e l i n e
if isSiri {
ctx . stroke ( curve , with : . linearGradient (
Gradient ( colors : fillColors ) ,
startPoint : CGPoint ( x : 0 , y : centerY ) ,
endPoint : CGPoint ( x : w , y : centerY )
) , style : strokeStyle )
} else {
ctx . stroke ( curve , with : . color ( strokeColor ) , style : strokeStyle )
}
}
2026-04-09 23:11:40 -07:00
// MARK: - S i r i W a v e
// / A u t h e n t i c m u l t i - l a y e r S i r i w a v e f o r m .
// /
// / F i v e s t r o k e d p a t h s a r e d r a w n o v e r t h e s a m e l e v e l d a t a . E a c h l a y e r h a s :
// / - A d i s t i n c t p h a s e o f f s e t s o t h e p e a k s w e a v e p a s t e a c h o t h e r o r g a n i c a l l y .
// / - A d i s t i n c t a m p l i t u d e m u l t i p l i e r ( s o m e n e g a t i v e = i n v e r t e d ) c r e a t i n g t h e
// / c r o s s i n g / f i g u r e - e i g h t s t r u c t u r e v i s i b l e i n t h e o r i g i n a l M i t s u h a I n f i n i t y .
// / - A s i n e - w i n d o w ( b e l l - c u r v e ) a t t e n u a t i o n a c r o s s t h e w i d t h : t h e w a v e i s f o r c e d
// / t o z e r o a t b o t h e d g e s a n d r e a c h e s f u l l a m p l i t u d e o n l y i n t h e c e n t r e .
// / M a t h e m a t i c a l l y : a t t e n u a t i o n ( x ) = s i n ( π * x / w ) w h i c h i s 0 a t x = 0 , p e a k s a t 1
// / a t x = w / 2 , a n d r e t u r n s t o 0 a t x = w .
// / - ` p l u s L i g h t e r ` b l e n d m o d e s o w h e r e t w o b r i g h t l i n e s o v e r l a p t h e y a d d t h e i r
// / l u m i n a n c e , c r e a t i n g g l o w i n g i n t e r s e c t i o n n o d e s l i k e t h e o r i g i n a l .
private func drawSiriWave ( ctx : GraphicsContext , size : CGSize , levels : [ Float ] , continuousTime : Double ) {
let w = size . width
let h = size . height
guard levels . count >= 2 else { return }
let baseLift : CGFloat = compact ? 0 : CGFloat ( settings . npBaseLift )
let waveOffset = compact ? 0 : CGFloat ( settings . waveOffsetTop )
let centerY = compact ? h * 0.5 : h - baseLift - waveOffset
let baseAmp : CGFloat = compact
? CGFloat ( settings . miniAmplitude ) * h * 0.4
: CGFloat ( settings . npAmplitude ) * h * 0.38
// F i v e l a y e r s : ( p h a s e o f f s e t i n r a d i a n s , a m p l i t u d e m u l t i p l i e r , o p a c i t y , s t r o k e w i d t h )
let layers : [ ( phase : Double , ampMult : CGFloat , opacity : Double , width : CGFloat ) ] = [
( phase : 0.0 , ampMult : 1.00 , opacity : 0.90 , width : 2.5 ) , // c o r e
( phase : . pi * 0.35 , ampMult : 0.60 , opacity : 0.65 , width : 1.8 ) , // s u p p o r t A
( phase : . pi * 0.70 , ampMult : 0.30 , opacity : 0.50 , width : 1.4 ) , // s u p p o r t B
( phase : . pi * 1.10 , ampMult : - 0.40 , opacity : 0.45 , width : 1.2 ) , // i n v e r t e d A
( phase : . pi * 1.55 , ampMult : - 0.20 , opacity : 0.30 , width : 1.0 ) , // i n v e r t e d B
]
let count = levels . count
let spacing = w / CGFloat ( count - 1 )
// B u i l d s i r i c o l o u r g r a d i e n t o n c e
let siriColors : [ Color ] = [ . pink , . purple , . cyan , . green , . pink ]
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
let useVibrant = config . colorMode = = . vibrant
let a = config . alpha
2026-04-09 23:11:40 -07:00
for layer in layers {
var points : [ CGPoint ] = [ ]
points . reserveCapacity ( count )
for i in 0. . < count {
let x = CGFloat ( i ) * spacing
let nx = x / w // n o r m a l i s e d 0 → 1
// S i n e - w i n d o w a t t e n u a t i o n — z e r o a t e d g e s , 1 a t c e n t r e
let attenuation = CGFloat ( sin ( . pi * nx ) )
// S a m p l e l e v e l a t t h i s x w i t h p e r - l a y e r p h a s e o f f s e t a p p l i e d a s a
// t i m e w a r p : t h e e f f e c t i v e s a m p l e p o s i t i o n s h i f t s a l o n g t h e l e v e l a r r a y
// b y ( p h a s e / 2 π ) * c o u n t , w r a p p e d w i t h l i n e a r i n t e r p o l a t i o n .
let shift = layer . phase / ( 2 * . pi ) * Double ( count - 1 )
let rawPos = Double ( i ) + shift
let lo = Int ( rawPos ) % count
let hi = ( lo + 1 ) % count
let frac = CGFloat ( rawPos - floor ( rawPos ) )
let sampledLevel = CGFloat ( levels [ lo ] ) * ( 1 - frac ) + CGFloat ( levels [ hi ] ) * frac
// O r g a n i c w o b b l e u s i n g c o n t i n u o u s t i m e + p e r - l a y e r p h a s e
let wobble = CGFloat ( sin ( continuousTime * 3.0 + Double ( i ) * 0.8 + layer . phase ) * 0.03 )
let totalAmp = ( sampledLevel + wobble ) * layer . ampMult * attenuation * baseAmp
points . append ( CGPoint ( x : x , y : centerY - totalAmp ) )
}
let curve = strokeableCurve ( points )
let style = StrokeStyle ( lineWidth : layer . width , lineCap : . round , lineJoin : . round )
// D r a w w i t h p l u s L i g h t e r b l e n d m o d e f o r a d d i t i v e g l o w a t i n t e r s e c t i o n s
ctx . drawLayer { layerCtx in
layerCtx . blendMode = . plusLighter
if useVibrant {
layerCtx . stroke ( curve , with : . linearGradient (
Gradient ( colors : siriColors . map { $0 . opacity ( layer . opacity * a ) } ) ,
startPoint : . zero ,
endPoint : CGPoint ( x : w , y : 0 )
) , style : style )
} else {
let c : Color
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
switch config . colorMode {
2026-04-09 23:11:40 -07:00
case . albumArt : c = AlbumColorExtractor . shared . primaryColor
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
case . custom : c = config . customColor
2026-04-09 23:11:40 -07:00
default : c = accentColor
}
layerCtx . stroke ( curve , with : . color ( c . opacity ( layer . opacity * a ) ) , style : style )
}
}
}
}
2026-03-28 13:49:47 -07:00
}
// MARK: - C o m p a c t V i s u a l i z e r ( f o r m i n i p l a y e r )
struct CompactVisualizerView : View {
let isPlaying : Bool
2026-04-05 08:33:00 -07:00
var isSongLoaded : Bool = true
2026-04-11 19:01:10 -07:00
var songId : String ? = nil
2026-03-28 13:49:47 -07:00
let accentColor : Color
let height : CGFloat
2026-04-05 08:33:00 -07:00
2026-03-28 13:49:47 -07:00
var body : some View {
MitsuhaVisualizerView (
isPlaying : isPlaying ,
2026-04-05 08:33:00 -07:00
isSongLoaded : isSongLoaded ,
2026-04-11 19:01:10 -07:00
songId : songId ,
2026-03-28 13:49:47 -07:00
accentColor : accentColor ,
compact : true
)
. frame ( height : height )
}
}
// MARK: - V i s u a l i z e r S e t t i n g s V i e w
struct VisualizerSettingsView : View {
@ ObservedObject var settings = VisualizerSettings . shared
@ Environment ( \ . dismiss ) private var dismiss
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
2026-03-28 13:49:47 -07:00
private let pink = Color ( red : 1.0 , green : 0.176 , blue : 0.333 )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
2026-03-28 13:49:47 -07:00
var body : some View {
NavigationStack {
Form {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// ─ ─ M a s t e r t o g g l e s ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
2026-03-28 13:49:47 -07:00
Section {
Toggle ( " Enabled " , isOn : $ settings . enabled ) . tint ( pink )
Toggle ( " Now Playing Screen " , isOn : $ settings . nowPlayingEnabled ) . tint ( pink ) . disabled ( ! settings . enabled )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
Toggle ( " Mini Player " , isOn : $ settings . miniPlayerEnabled ) . tint ( pink ) . disabled ( ! settings . enabled )
2026-03-28 13:49:47 -07:00
} header : { Text ( " VISUALIZER " ) } footer : {
Text ( " Master toggle disables all visualizers. Individual toggles control each location. " )
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// ─ ─ P e r - v i e w i n d e p e n d e n t c o n f i g s ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
2026-03-28 13:49:47 -07:00
Section {
NavigationLink {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
ViewConfigSettingsView (
title : " Now Playing " ,
config : $ settings . nowPlaying ,
amplitude : $ settings . npAmplitude ,
baseLift : $ settings . npBaseLift ,
waveOffset : $ settings . waveOffsetTop ,
depthOffset : $ settings . depthOffset ,
depthOpacity : $ settings . depthOpacity ,
idleAmplitude : $ settings . idleAmplitude ,
heightPct : $ settings . nowPlayingHeightPct ,
isCompact : false
)
2026-03-28 13:49:47 -07:00
} label : {
HStack {
Image ( systemName : " play.rectangle.fill " ) . foregroundColor ( pink ) . frame ( width : 28 )
Text ( " Now Playing " )
Spacer ( )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
Text ( settings . nowPlaying . style . rawValue )
. font ( . caption ) . foregroundColor ( . gray )
2026-03-28 13:49:47 -07:00
}
}
NavigationLink {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
ViewConfigSettingsView (
title : " Mini Player " ,
config : $ settings . miniPlayer ,
amplitude : $ settings . miniAmplitude ,
baseLift : . constant ( 0 ) ,
waveOffset : . constant ( 0 ) ,
depthOffset : $ settings . miniDepthOffset ,
depthOpacity : $ settings . miniDepthOpacity ,
idleAmplitude : $ settings . miniIdleAmplitude ,
heightPct : . constant ( 0 ) ,
isCompact : true
)
2026-03-28 13:49:47 -07:00
} label : {
HStack {
Image ( systemName : " rectangle.bottomhalf.filled " ) . foregroundColor ( pink ) . frame ( width : 28 )
Text ( " Mini Player " )
Spacer ( )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
Text ( settings . miniPlayer . style . rawValue )
. font ( . caption ) . foregroundColor ( . gray )
2026-03-28 13:49:47 -07:00
}
}
} header : { Text ( " PER-VIEW SETTINGS " ) } footer : {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
Text ( " Each view has its own style, color, point count, sensitivity, amplitude, and depth. Changes in one view never affect the other. " )
2026-03-28 13:49:47 -07:00
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// ─ ─ S t y l e - s p e c i f i c g l o b a l p a r a m s ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
Section {
sliderRowDouble ( " Wave stroke " , value : $ settings . waveStrokeThickness , range : 0.5 . . . 5.0 , step : 0.5 , format : " %.1f " )
sliderRowDouble ( " Bar spacing " , value : $ settings . barSpacing , range : 0. . . 20 , step : 1 , format : " %.0f " )
sliderRowDouble ( " Bar radius " , value : $ settings . barCornerRadius , range : 0. . . 15 , step : 1 , format : " %.0f " )
sliderRowDouble ( " Line thickness " , value : $ settings . lineThickness , range : 1. . . 12 , step : 0.5 , format : " %.1f " )
} header : { Text ( " STYLE GLOBALS " ) } footer : {
Text ( " These shape parameters apply globally to their respective styles across both views. " )
}
// ─ ─ S h a r e d p h y s i c s ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
2026-03-28 13:49:47 -07:00
Section {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
sliderRowDouble ( " Viscosity " , value : $ settings . viscosity , range : 0.05 . . . 1.0 , step : 0.01 , format : " %.2f " )
sliderRow ( " Frequency cutoff " , value : $ settings . frequencyCutoff , range : 40. . . 150 , step : 5 )
sliderRowDouble ( " Base multiplier " , value : $ settings . baseMultiplier , range : 5.0 . . . 50.0 , step : 1.0 , format : " %.0f " )
sliderRowDouble ( " FPS " , value : $ settings . fps , range : 15. . . 60 , step : 1 , format : " %.0f " )
2026-03-28 13:49:47 -07:00
Toggle ( " Real Audio Analysis " , isOn : $ settings . realAudioAnalysis ) . tint ( pink )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
Toggle ( " Dynamic Gain " , isOn : $ settings . dynamicGainEnabled ) . tint ( pink )
} header : { Text ( " SHARED PHYSICS " ) } footer : {
Text ( " Viscosity: 0.10– 0.20 = heavy ocean swell. 0.4+ = snappy EQ. Base Multiplier: 15– 25 for most music. Dynamic Gain normalises amplitude across loud and quiet tracks. " )
2026-03-28 13:49:47 -07:00
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// ─ ─ P r e s e t s ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
2026-03-28 13:49:47 -07:00
Section {
Button ( action : applyDeepOcean ) {
HStack {
Image ( systemName : " water.waves " ) . foregroundColor ( . cyan )
VStack ( alignment : . leading ) {
Text ( " Deep Ocean Swell " ) . foregroundColor ( . white )
Text ( " Slow, massive rolling waves " ) . font ( . caption2 ) . foregroundColor ( . gray )
}
}
}
Button ( action : applyReactiveEQ ) {
HStack {
Image ( systemName : " waveform " ) . foregroundColor ( . green )
VStack ( alignment : . leading ) {
Text ( " Reactive EQ " ) . foregroundColor ( . white )
Text ( " Detailed, fast response " ) . font ( . caption2 ) . foregroundColor ( . gray )
}
}
}
Button ( action : applySubtleAmbient ) {
HStack {
Image ( systemName : " moonphase.waning.crescent " ) . foregroundColor ( . purple )
VStack ( alignment : . leading ) {
Text ( " Subtle Ambient " ) . foregroundColor ( . white )
Text ( " Gentle, transparent shimmer " ) . font ( . caption2 ) . foregroundColor ( . gray )
}
}
}
Button ( action : applyHeavyMercury ) {
HStack {
Image ( systemName : " drop.fill " ) . foregroundColor ( . gray )
VStack ( alignment : . leading ) {
Text ( " Heavy Liquid Mercury " ) . foregroundColor ( . white )
Text ( " Dense, weighty metallic flow " ) . font ( . caption2 ) . foregroundColor ( . gray )
}
}
}
} header : { Text ( " PRESETS " ) } footer : {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
Text ( " Presets apply to the Now Playing view. Fine-tune in Per-View Settings afterwards. " )
2026-03-28 13:49:47 -07:00
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
2026-03-28 13:49:47 -07:00
Section { Button ( " Reset to Defaults " , role : . destructive ) { resetDefaults ( ) } }
}
. navigationTitle ( " Visualizer " )
. navigationBarTitleDisplayMode ( . inline )
. toolbar {
ToolbarItem ( placement : . navigationBarTrailing ) {
Button ( " Done " ) { dismiss ( ) } . foregroundColor ( pink )
}
}
}
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
2026-03-28 13:49:47 -07:00
// MARK: - P r e s e t s
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
2026-03-28 13:49:47 -07:00
private func applyDeepOcean ( ) {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
settings . nowPlaying . wavePoints = 6
settings . viscosity = 0.12 ; settings . baseMultiplier = 22 ; settings . frequencyCutoff = 50
settings . npAmplitude = 0.75 ; settings . depthOffset = 30
settings . nowPlaying . waveSensitivity = 0.8
2026-03-28 13:49:47 -07:00
}
private func applyReactiveEQ ( ) {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
settings . nowPlaying . barPoints = 18 ; settings . nowPlaying . style = . bar
settings . viscosity = 0.6 ; settings . baseMultiplier = 20 ; settings . frequencyCutoff = 120
settings . npAmplitude = 0.5 ; settings . depthOffset = 5
settings . nowPlaying . barSensitivity = 1.4
2026-03-28 13:49:47 -07:00
}
private func applySubtleAmbient ( ) {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
settings . nowPlaying . wavePoints = 8 ; settings . nowPlaying . style = . wave
settings . viscosity = 0.18 ; settings . baseMultiplier = 15 ; settings . idleAmplitude = 0.04
settings . npAmplitude = 0.25 ; settings . nowPlaying . alpha = 0.3
settings . nowPlaying . waveSensitivity = 0.7
2026-03-28 13:49:47 -07:00
}
private func applyHeavyMercury ( ) {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
settings . nowPlaying . wavePoints = 10 ; settings . nowPlaying . style = . wave
settings . viscosity = 0.12 ; settings . baseMultiplier = 60
settings . npAmplitude = 0.6 ; settings . depthOffset = 20
settings . nowPlaying . waveSensitivity = 2.5
2026-03-28 13:49:47 -07:00
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// MARK: - S l i d e r h e l p e r s
2026-03-28 13:49:47 -07:00
func sliderRow ( _ label : String , value : Binding < Int > , range : ClosedRange < Int > , step : Int ) -> some View {
VStack ( alignment : . leading , spacing : 4 ) {
Text ( label ) . font ( . body )
HStack {
Slider ( value : Binding ( get : { Double ( value . wrappedValue ) } , set : { value . wrappedValue = Int ( $0 ) } ) ,
in : Double ( range . lowerBound ) . . . Double ( range . upperBound ) , step : Double ( step ) ) . tint ( pink )
Text ( " \( value . wrappedValue ) " ) . foregroundColor ( . gray ) . frame ( width : 44 , alignment : . trailing )
}
}
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
2026-03-28 13:49:47 -07:00
func sliderRowDouble ( _ label : String , value : Binding < Double > , range : ClosedRange < Double > , step : Double , format : String ) -> some View {
VStack ( alignment : . leading , spacing : 4 ) {
Text ( label ) . font ( . body )
HStack {
Slider ( value : value , in : range , step : step ) . tint ( pink )
Text ( String ( format : format , value . wrappedValue ) ) . foregroundColor ( . gray ) . frame ( width : 44 , alignment : . trailing )
}
}
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
2026-03-28 13:49:47 -07:00
private func resetDefaults ( ) {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
settings . nowPlaying = . nowPlayingDefault
settings . miniPlayer = . miniPlayerDefault
2026-03-28 13:49:47 -07:00
settings . enabled = true ; settings . nowPlayingEnabled = true ; settings . miniPlayerEnabled = true
2026-04-10 07:00:30 -07:00
settings . fps = 60 ; settings . realAudioAnalysis = true ; settings . dynamicGainEnabled = true
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
settings . waveStrokeThickness = 1.5 ; settings . barSpacing = 5 ; settings . barCornerRadius = 0 ; settings . lineThickness = 5
settings . viscosity = 0.17 ; settings . frequencyCutoff = 75 ; settings . baseMultiplier = 18.0
settings . waveOffsetTop = 0 ; settings . nowPlayingHeightPct = 0.50
settings . npAmplitude = 0.50 ; settings . npBaseLift = 130.0
2026-04-10 07:00:30 -07:00
settings . depthOffset = 14.0 ; settings . depthOpacity = 0.18 ; settings . idleAmplitude = 0.03
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
settings . miniPlayerHeight = 48.0 ; settings . miniOpacity = 0.5
settings . miniAmplitude = 0.7 ; settings . miniIdleAmplitude = 0.03
2026-03-28 13:49:47 -07:00
settings . miniDepthOffset = 8.0 ; settings . miniDepthOpacity = 0.2
}
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// MARK: - P e r - V i e w C o n f i g S e t t i n g s ( N o w P l a y i n g & M i n i P l a y e r s h a r e t h i s )
struct ViewConfigSettingsView : View {
let title : String
@ Binding var config : ViewVisualizerConfig
@ Binding var amplitude : Double
@ Binding var baseLift : Double
@ Binding var waveOffset : Double
@ Binding var depthOffset : Double
@ Binding var depthOpacity : Double
@ Binding var idleAmplitude : Double
@ Binding var heightPct : Double
var isCompact : Bool
@ ObservedObject private var settings = VisualizerSettings . shared
2026-03-28 13:49:47 -07:00
private let pink = Color ( red : 1.0 , green : 0.176 , blue : 0.333 )
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
2026-03-28 13:49:47 -07:00
var body : some View {
Form {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// ─ ─ S t y l e ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
Section {
Picker ( " Style " , selection : $ config . style ) {
ForEach ( VisualizerSettings . Style . allCases , id : \ . self ) { Text ( $0 . rawValue ) . tag ( $0 ) }
} . pickerStyle ( . segmented )
// P e r - s t y l e p o i n t c o u n t — i n d e p e n d e n t , n o t s h a r e d w i t h o t h e r s t y l e
sd ( " Points ( \( config . style . rawValue ) ) " ,
value : Binding (
get : { Double ( config . numberOfPoints ) } ,
set : { config . numberOfPoints = Int ( $0 ) }
) ,
range : 4. . . 24 , step : 1 , format : " %.0f " )
// P e r - s t y l e s e n s i t i v i t y
sd ( " Sensitivity ( \( config . style . rawValue ) ) " ,
value : Binding (
get : { config . sensitivity } ,
set : { config . sensitivity = $0 }
) ,
range : 0.1 . . . 2.0 , step : 0.1 , format : " %.1f " )
} header : { Text ( " STYLE " ) } footer : {
Text ( " Each style remembers its own point count and sensitivity independently. Switching styles never overwrites another style's values. " )
}
// ─ ─ C o l o r ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
Section {
Picker ( " Color " , selection : $ config . colorMode ) {
ForEach ( VisualizerSettings . ColorMode . allCases , id : \ . self ) { Text ( $0 . rawValue ) . tag ( $0 ) }
} . pickerStyle ( . segmented )
sd ( " Alpha " , value : $ config . alpha , range : 0.1 . . . 1.0 , step : 0.05 , format : " %.2f " )
if config . colorMode = = . custom {
ColorPicker ( " Wave Color " , selection : Binding (
get : { config . customColor } ,
set : { c in
if let components = UIColor ( c ) . cgColor . components , components . count >= 3 {
config . customColorR = Double ( components [ 0 ] )
config . customColorG = Double ( components [ 1 ] )
config . customColorB = Double ( components [ 2 ] )
}
}
) )
}
} header : { Text ( " COLOR " ) }
// ─ ─ A m p l i t u d e & L a y o u t ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
2026-03-28 13:49:47 -07:00
Section {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
if ! isCompact {
VStack ( alignment : . leading , spacing : 4 ) {
Text ( " Screen height % " ) . font ( . body )
HStack {
Slider ( value : $ heightPct , in : 0.2 . . . 0.8 , step : 0.05 ) . tint ( pink )
Text ( " \( Int ( heightPct * 100 ) ) % " ) . foregroundColor ( . gray ) . frame ( width : 52 , alignment : . trailing )
}
2026-03-28 13:49:47 -07:00
}
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
sd ( " Amplitude " , value : $ amplitude , range : 0.1 . . . 1.0 , step : 0.05 , format : " %.2f " )
if ! isCompact {
sd ( " Base lift (from bottom) " , value : $ baseLift , range : 0. . . 300 , step : 5 , format : " %.0f pt " )
sd ( " Wave offset (top) " , value : $ waveOffset , range : - 100. . . 300 , step : 5 , format : " %.0f " )
}
} header : { Text ( " LAYOUT & AMPLITUDE " ) }
// ─ ─ D e p t h & I d l e ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
2026-03-28 13:49:47 -07:00
Section {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
sd ( " Depth offset " , value : $ depthOffset , range : 0. . . 50 , step : 2 , format : " %.0f " )
sd ( " Depth opacity " , value : $ depthOpacity , range : 0.0 . . . 0.5 , step : 0.05 , format : " %.2f " )
sd ( " Idle amplitude " , value : $ idleAmplitude , range : 0.0 . . . 0.15 , step : 0.005 , format : " %.3f " )
} header : { Text ( " DEPTH & IDLE " ) }
// ─ ─ P r e v i e w ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
2026-03-28 13:49:47 -07:00
Section {
ZStack {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
Color ( white : isCompact ? 0.12 : 0.0 )
MitsuhaVisualizerView ( previewLevels : previewLevels , isPlaying : true ,
accentColor : pink , compact : isCompact )
. frame ( height : isCompact ? settings . miniPlayerHeight : 180 )
. opacity ( isCompact ? settings . miniOpacity : 1.0 )
if isCompact {
HStack ( spacing : 12 ) {
RoundedRectangle ( cornerRadius : 4 ) . fill ( Color . gray . opacity ( 0.3 ) ) . frame ( width : 40 , height : 40 )
VStack ( alignment : . leading , spacing : 2 ) {
Text ( " Song Title " ) . font ( . system ( size : 14 , weight : . medium ) ) . foregroundColor ( . white )
Text ( " Artist " ) . font ( . system ( size : 12 ) ) . foregroundColor ( . gray )
}
Spacer ( )
Image ( systemName : " pause.fill " ) . foregroundColor ( . white )
Image ( systemName : " forward.fill " ) . foregroundColor ( . white )
} . padding ( . horizontal , 12 )
}
2026-03-28 13:49:47 -07:00
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
. frame ( height : isCompact ? 64 : 180 )
. listRowInsets ( EdgeInsets ( ) )
. listRowBackground ( Color ( white : isCompact ? 0.12 : 0.0 ) )
2026-03-28 13:49:47 -07:00
} header : { Text ( " PREVIEW " ) }
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
. navigationTitle ( title )
. navigationBarTitleDisplayMode ( . inline )
2026-03-28 13:49:47 -07:00
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
private var previewLevels : [ Float ] {
( 0. . < 30 ) . map { Float ( 0.2 + 0.5 * sin ( Float ( $0 ) * 0.4 ) ) }
}
2026-03-28 13:49:47 -07:00
private func sd ( _ label : String , value : Binding < Double > , range : ClosedRange < Double > , step : Double , format : String ) -> some View {
VStack ( alignment : . leading , spacing : 4 ) {
Text ( label ) . font ( . body )
HStack {
Slider ( value : value , in : range , step : step ) . tint ( pink )
Text ( String ( format : format , value . wrappedValue ) ) . foregroundColor ( . gray ) . frame ( width : 52 , alignment : . trailing )
}
}
}
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
// MARK: - L e g a c y s u b - s e t t i n g s v i e w s ( k e p t f o r b a c k w a r d c o m p a t , n o w r e d i r e c t t o u n i f i e d v i e w )
struct NowPlayingVisSettingsView : View {
2026-03-28 13:49:47 -07:00
@ ObservedObject var settings : VisualizerSettings
var body : some View {
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
ViewConfigSettingsView (
title : " Now Playing " ,
config : $ settings . nowPlaying ,
amplitude : $ settings . npAmplitude ,
baseLift : $ settings . npBaseLift ,
waveOffset : $ settings . waveOffsetTop ,
depthOffset : $ settings . depthOffset ,
depthOpacity : $ settings . depthOpacity ,
idleAmplitude : $ settings . idleAmplitude ,
heightPct : $ settings . nowPlayingHeightPct ,
isCompact : false
)
2026-03-28 13:49:47 -07:00
}
bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
}
struct MiniPlayerVisSettingsView : View {
@ ObservedObject var settings : VisualizerSettings
var body : some View {
ViewConfigSettingsView (
title : " Mini Player " ,
config : $ settings . miniPlayer ,
amplitude : $ settings . miniAmplitude ,
baseLift : . constant ( 0 ) ,
waveOffset : . constant ( 0 ) ,
depthOffset : $ settings . miniDepthOffset ,
depthOpacity : $ settings . miniDepthOpacity ,
idleAmplitude : $ settings . miniIdleAmplitude ,
heightPct : . constant ( 0 ) ,
isCompact : true
)
2026-03-28 13:49:47 -07:00
}
}