bug fixes and ready to upload to testflight

This commit is contained in:
Dallas Groot 2026-04-10 19:05:45 -07:00
parent ea50bd4537
commit a4103c8250
4 changed files with 145 additions and 80 deletions

View file

@ -840,6 +840,7 @@ class AudioPlayer: NSObject, ObservableObject {
// Without this, the first 1-2 Canvas frames read stale zeroed _audioLevels
// (zeroed by pause) and the wave starts flat then slowly ramps up.
if isUsingOfflineVis {
alog("resume: offlineVis path — startOfflineVisSync", category: "VisDebug")
// Offline vis: prime with the last-rendered frame at current position
if !offlineVisBuffer.isEmpty {
let dur = duration
@ -850,6 +851,7 @@ class AudioPlayer: NSObject, ObservableObject {
}
startOfflineVisSync()
} else if isUsingRealFFT, !isUsingOfflineVis {
alog("resume: realFFT path — rebuilding levelTimer", category: "VisDebug")
// Engine FFT path: rawFFTLevels still holds pre-pause data push it immediately
setLevels(rawFFTLevels)
stopLevelTimer()
@ -858,6 +860,7 @@ class AudioPlayer: NSObject, ObservableObject {
self.setLevels(self.rawFFTLevels)
}
} else {
alog("resume: simulation path — levelTimer nil=\(levelTimer == nil)", category: "VisDebug")
// Simulation path: seed internalLevels from a fresh random target
// so the wave has height immediately rather than starting from floor.
lastTargetPhase = -1

View file

@ -20,7 +20,14 @@ struct MainTabView: View {
// Debug panel (docked)
@State private var debugPanelHeight: CGFloat = 250
@State private var debugDragOffset: CGFloat = 0
@State private var showFullConsole = false
// Group toggles same @AppStorage keys as DebugConsoleView so state is shared
@AppStorage("dbg_show_iphone") private var dbgShowIPhone = true
@AppStorage("dbg_show_watch") private var dbgShowWatch = true
@AppStorage("dbg_show_companion") private var dbgShowCompanion = true
@AppStorage("dbg_show_audio") private var dbgShowAudio = true
// Debug PiP (floating)
@State private var debugPipMode = true
@State private var pipPosition: CGPoint = CGPoint(x: 16, y: 100)
@ -207,7 +214,7 @@ struct MainTabView: View {
}
// MARK: - Debug Panel
private var debugPanel: some View {
VStack(spacing: 0) {
// Drag handle + header
@ -216,86 +223,62 @@ struct MainTabView: View {
.fill(Color.white.opacity(0.4))
.frame(width: 36, height: 4)
.padding(.top, 8)
HStack {
Image(systemName: "ladybug.fill")
.font(.system(size: 12))
.foregroundColor(.red)
Text("Debug Console")
.font(.system(size: 13, weight: .semibold))
.foregroundColor(.white)
Text("\(DebugLogger.shared.entries.count)")
Image(systemName: "ladybug.fill").font(.system(size: 12)).foregroundColor(.red)
Text("Debug Console").font(.system(size: 13, weight: .semibold)).foregroundColor(.white)
Text("\(filteredDebugEntries.count)/\(DebugLogger.shared.entries.count)")
.font(.system(size: 10, weight: .medium, design: .monospaced))
.foregroundColor(.gray)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.white.opacity(0.1))
.cornerRadius(8)
.padding(.horizontal, 6).padding(.vertical, 2)
.background(Color.white.opacity(0.1)).cornerRadius(8)
Spacer()
Button(action: { DebugLogger.shared.clear() }) {
Text("Clear")
.font(.system(size: 12, weight: .medium))
.foregroundColor(.red.opacity(0.8))
Text("Clear").font(.system(size: 12, weight: .medium)).foregroundColor(.red.opacity(0.8))
}
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
debugPipMode = true
}
}) {
Image(systemName: "pip")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.6))
Button(action: { withAnimation(.easeInOut(duration: 0.3)) { debugPipMode = true } }) {
Image(systemName: "pip").font(.system(size: 14)).foregroundColor(.white.opacity(0.6))
}
Button(action: {
debugLogger.isEnabled = false
}) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 16))
.foregroundColor(.gray)
// Open full console with all controls
Button(action: { showFullConsole = true }) {
Image(systemName: "arrow.up.left.and.arrow.down.right")
.font(.system(size: 13)).foregroundColor(.white.opacity(0.6))
}
Button(action: { debugLogger.isEnabled = false }) {
Image(systemName: "xmark.circle.fill").font(.system(size: 16)).foregroundColor(.gray)
}
}
.padding(.horizontal, 14)
.padding(.horizontal, 14).padding(.bottom, 4)
// Group toggles
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 6) {
dbgGroupToggle("iPhone", icon: "iphone", isOn: $dbgShowIPhone)
dbgGroupToggle("Watch", icon: "applewatch", isOn: $dbgShowWatch)
dbgGroupToggle("Companion", icon: "server.rack", isOn: $dbgShowCompanion)
dbgGroupToggle("Audio", icon: "waveform", isOn: $dbgShowAudio)
}
.padding(.horizontal, 14)
}
.padding(.bottom, 6)
}
.background(Color(white: 0.1))
.gesture(
DragGesture()
.onChanged { value in
debugDragOffset = value.translation.height
}
.onEnded { value in
let newHeight = debugPanelHeight - value.translation.height
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
debugDragOffset = 0
debugPanelHeight = min(max(newHeight, 120), 500)
}
}
)
Divider().background(Color.white.opacity(0.15))
// Log entries
// Log entries filtered by group toggles
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 1) {
ForEach(DebugLogger.shared.entries) { entry in
debugLogRow(entry)
.id(entry.id)
ForEach(filteredDebugEntries) { entry in
debugLogRow(entry).id(entry.id)
}
}
.padding(.horizontal, 10)
.padding(.vertical, 4)
.padding(.horizontal, 10).padding(.vertical, 4)
}
.onChange(of: DebugLogger.shared.entries.count) { _, _ in
if let last = DebugLogger.shared.entries.last {
withAnimation(.easeOut(duration: 0.15)) {
proxy.scrollTo(last.id, anchor: .bottom)
}
if let last = filteredDebugEntries.last {
withAnimation(.easeOut(duration: 0.15)) { proxy.scrollTo(last.id, anchor: .bottom) }
}
}
}
@ -305,7 +288,48 @@ struct MainTabView: View {
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.shadow(color: .black.opacity(0.4), radius: 8, y: -2)
.padding(.horizontal, 4)
.padding(.bottom, 49) // above tab bar
.padding(.bottom, 49)
.sheet(isPresented: $showFullConsole) { DebugConsoleView() }
.gesture(
DragGesture()
.onChanged { value in debugDragOffset = value.translation.height }
.onEnded { value in
let newHeight = debugPanelHeight - value.translation.height
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
debugDragOffset = 0
debugPanelHeight = min(max(newHeight, 120), 500)
}
}
)
}
// Filtered entries for the docked panel using the same group toggle AppStorage keys
private var filteredDebugEntries: [DebugLogger.LogEntry] {
let watchCats: Set<String> = ["Watch", "WC", "WatchSync"]
let companionCats: Set<String> = ["Companion", "SmartDJ", "Crossfade", "Sync"]
let audioCats: Set<String> = ["Audio", "FFT", "Radio", "Prefetch"]
return DebugLogger.shared.entries.filter { entry in
if entry.isMarker { return true }
let isWatch = watchCats.contains(entry.category)
let isCompanion = companionCats.contains(entry.category)
let isAudio = audioCats.contains(entry.category)
let isIPhone = !isWatch && !isCompanion && !isAudio
return (isIPhone && dbgShowIPhone) || (isWatch && dbgShowWatch) ||
(isCompanion && dbgShowCompanion) || (isAudio && dbgShowAudio)
}
}
private func dbgGroupToggle(_ title: String, icon: String, isOn: Binding<Bool>) -> some View {
Button(action: { isOn.wrappedValue.toggle() }) {
HStack(spacing: 4) {
Image(systemName: icon).font(.system(size: 10))
Text(title).font(.system(size: 10, weight: .medium))
}
.foregroundColor(isOn.wrappedValue ? .white : .gray.opacity(0.4))
.padding(.horizontal, 8).padding(.vertical, 4)
.background(isOn.wrappedValue ? accentPink.opacity(0.4) : Color.white.opacity(0.05))
.cornerRadius(10)
}
}
private func debugLogRow(_ entry: DebugLogger.LogEntry) -> some View {

View file

@ -372,35 +372,42 @@ struct SearchView: View {
guard !songsLoaded else { return }
isLoadingSongs = true
// Try loading from cache first for instant display
// Cache hit instant display
if let cached = libraryCache.load([Song].self, key: "all_songs_sorted") {
await MainActor.run {
allSongs = cached
allSongs = cached
isLoadingSongs = false
songsLoaded = true
songsLoaded = true
}
return
}
// Fetch all albums, then their songs, sorted alphabetically by title
// Use search3 with empty query to fetch all songs directly.
// This is a single paginated call per 500 songs vs. 1 call per album
// (144 albums = 144 sequential requests, many of which fail silently).
do {
var offset = 0
var collected: [Song] = []
var offset = 0
let pageSize = 500
while true {
let albums = try await serverManager.client.getAlbumList2(
type: "alphabeticalByName", size: 500, offset: offset)
for album in albums {
if let detail = try? await serverManager.client.getAlbum(id: album.id) {
collected.append(contentsOf: detail.song ?? [])
}
}
if albums.count < 500 { break }
offset += 500
let result = try await serverManager.client.search3(
query: "",
artistCount: 0, artistOffset: 0,
albumCount: 0, albumOffset: 0,
songCount: pageSize,
songOffset: offset
)
let page = result?.song ?? []
collected.append(contentsOf: page)
if page.count < pageSize { break }
offset += pageSize
}
let sorted = collected.sorted {
$0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending
}
let sorted = collected.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending }
libraryCache.save(sorted, key: "all_songs_sorted")
await MainActor.run {
allSongs = sorted
allSongs = sorted
isLoadingSongs = false
songsLoaded = true
}

View file

@ -256,6 +256,7 @@ fileprivate final class VisualizerLevelBox: ObservableObject {
var historyWriteIdx: Int = 0
var wobblePhaseOffset: Double = 0
var lastTickTime: CFTimeInterval = 0
var resumeTickCount: Int = 0 // debug: counts first N ticks after resume
/// Resize all per-frame buffers when point count changes (rare settings slider).
func resizeIfNeeded(count: Int, idleAmplitude: Float) {
@ -314,6 +315,16 @@ struct MitsuhaVisualizerView: View {
idleAmplitude: Float(settings.idleAmplitude))
let t = CACurrentMediaTime()
let rawLevels = previewLevels ?? AudioPlayer.shared.currentLevels()
// Log first 3 ticks after resume so we can see the TimelineView IS firing
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)
}
updateDisplayLevels(newRawLevels: rawLevels, t: t)
updateWobblePhase(t: t)
guard box.displayLevels.count >= 2 else { return }
@ -353,6 +364,19 @@ struct MitsuhaVisualizerView: View {
box.levelHistoryBuf.removeAll(keepingCapacity: true)
box.historyWriteIdx = 0
box.peakFollower = 0.01
box.lastTickTime = 0 // reset so first resume frame uses 1/60 fallback dt
DebugLogger.shared.log(
"PAUSE → levelHistoryBuf cleared, lastTickTime reset " +
"[targets:\(box.targetLevels.count) display:\(box.displayLevels.count)]",
category: "VisDebug", level: .info)
} else {
box.resumeTickCount = 0 // reset so we log the first 3 ticks
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)
}
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
@ -394,7 +418,14 @@ struct MitsuhaVisualizerView: View {
private func updateDisplayLevels(newRawLevels: [Float], t: Double) -> Bool {
let count = config.numberOfPoints
guard count > 0, !newRawLevels.isEmpty,
box.targetLevels.count == count else { return false }
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
}
let dt = Float(box.lastTickTime > 0 ? min(t - box.lastTickTime, 0.1) : 1.0/60.0)
let sens = Float(config.sensitivity)