bug fixes and ready to upload to testflight
This commit is contained in:
parent
ea50bd4537
commit
a4103c8250
4 changed files with 145 additions and 80 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue