From 619e922a278c9179e1af7d0bc55d0efc69a3ed80 Mon Sep 17 00:00:00 2001 From: Dallas Groot Date: Fri, 10 Apr 2026 00:39:18 -0700 Subject: [PATCH] visualizer still bugged, not updating --- .../Visualizer/MitsuhaVisualizerView.swift | 79 +++++++++---------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/iOS/Views/Visualizer/MitsuhaVisualizerView.swift b/iOS/Views/Visualizer/MitsuhaVisualizerView.swift index b7d9fb6..623cfdc 100644 --- a/iOS/Views/Visualizer/MitsuhaVisualizerView.swift +++ b/iOS/Views/Visualizer/MitsuhaVisualizerView.swift @@ -177,12 +177,16 @@ struct MitsuhaVisualizerView: View { TimelineView(.animation) { timeline in if settings.enabled { Canvas { context, size in - // timeline.date changes every frame — SwiftUI must re-execute this closure. - let t = timeline.date.timeIntervalSinceReferenceDate + // Use CACurrentMediaTime for consistent delta-time across both functions. + // Previously updateDisplayLevels used CACurrentMediaTime() but + // updateWobblePhase stored lastTickTime from timeIntervalSinceReferenceDate — + // mixing two clocks made dt ≈ -753,000,000, clamped to 0.001, giving + // smoothFactor ≈ 0.01 and a ~2-second response lag. + let t = CACurrentMediaTime() let rawLevels: [Float] = isPlaying ? (previewLevels ?? AudioPlayer.shared.currentLevels()) : Array(repeating: Float(0), count: max(settings.numberOfPoints, 1)) - updateDisplayLevels(newRawLevels: rawLevels) + updateDisplayLevels(newRawLevels: rawLevels, t: t) updateWobblePhase(t: t) let pts = box.displayLevels.isEmpty @@ -231,13 +235,17 @@ struct MitsuhaVisualizerView: View { // MARK: - Temporal Smoothing & Log Binning @discardableResult - private func updateDisplayLevels(newRawLevels: [Float]) -> Bool { + private func updateDisplayLevels(newRawLevels: [Float], t: Double) -> Bool { let count = settings.numberOfPoints guard count > 0, !newRawLevels.isEmpty else { return false } - + + // Delta time from the same CACurrentMediaTime clock as updateWobblePhase. + // lastTickTime is 0 on first frame — fall back to 60fps assumption. + let dt = Float(box.lastTickTime > 0 ? min(t - box.lastTickTime, 0.1) : 1.0/60.0) + let sens = Float(settings.sensitivity) let isPreProcessed = AudioPlayer.shared.isUsingOfflineVis - + var targetLevels: [Float] if isPreProcessed { @@ -257,40 +265,29 @@ struct MitsuhaVisualizerView: View { } } } else { - // Raw FFT bins or simulated levels — full log binning + EQ boost + // Raw FFT bins or simulated levels — full log binning targetLevels = [Float](repeating: 0, count: count) let maxUsefulBin = min(newRawLevels.count - 1, settings.frequencyCutoff) for i in 0.. 0 ? (sum / Float(countInBand)) : 0 targetLevels[i] = min(1.0, averageInBand * Float(settings.baseMultiplier) * sens) } } - // Temporal Smoothing — delta-time based so it's frame-rate independent. - // Use elapsed time since last tick to compute smoothFactor rather than - // assuming a fixed fps — this prevents lag when running at 60/120fps. - let dt = Float(max(box.lastTickTime > 0 ? min(CACurrentMediaTime() - box.lastTickTime, 0.1) : 1.0/60.0, 0.001)) - // viscosity 0.05 = very slow, 1.0 = instant. Scale by 60*dt so behaviour - // at 60fps matches the original fpsScale=1 viscosity settings. + // Temporal smoothing — viscosity 0.17 at 60fps → smoothFactor ≈ 0.17 per frame let smoothFactor = min(Float(settings.viscosity) * 60.0 * dt, 1.0) // ── Dynamic Gain / Peak Follower ───────────────────────────────────── @@ -787,15 +784,15 @@ struct VisualizerSettingsView: View { } Section { - sliderRowDouble("Viscosity", value: $settings.viscosity, range: 0.05...1.0, step: 0.05, format: "%.2f") - sliderRow("Frequency cutoff", value: $settings.frequencyCutoff, range: 40...200, step: 10) - sliderRowDouble("Base multiplier", value: $settings.baseMultiplier, range: 10.0...100.0, step: 5.0, format: "%.0f") - sliderRowDouble("Sensitivity", value: $settings.sensitivity, range: 0.1...4.0, step: 0.1, format: "%.1f") + 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("Sensitivity", value: $settings.sensitivity, range: 0.1...2.0, step: 0.1, format: "%.1f") sliderRowDouble("FPS", value: $settings.fps, range: 15...60, step: 1, format: "%.0f") Toggle("Real Audio Analysis", isOn: $settings.realAudioAnalysis).tint(pink) Toggle("Dynamic Gain", isOn: $settings.dynamicGainEnabled).tint(pink) } header: { Text("SHARED / ADVANCED") } footer: { - Text("Viscosity controls how quickly the wave reacts. 0.15–0.25 = heavy liquid. 0.5 = responsive. 0.8+ = snappy EQ.\n\nSensitivity multiplies incoming audio. Base multiplier is a second gain stage for FFT.\n\nDynamic Gain automatically normalises the wave amplitude across loud and quiet tracks — keeps the wave full on soft passages without clipping on loud ones.\n\nFPS drops to 24 in Low Power Mode.") + Text("Viscosity: 0.10–0.20 = heavy, slow liquid (Mitsuha). 0.4+ = snappy EQ. This is the most important slider.\n\nSensitivity: multiplies incoming audio. 0.8–1.0 is the sweet spot. Above 1.5 causes the wave to clip at the top.\n\nBase Multiplier: overall gain for FFT data. 15–25 works well. Higher values are needed for quieter recordings.\n\nFrequency Cutoff: limits how many FFT bins are used. 60–80 gives mostly bass and mids. 100+ adds treble detail.\n\nDynamic Gain: recommended ON — normalises amplitude across loud and quiet tracks.") } // Presets @@ -855,16 +852,16 @@ struct VisualizerSettingsView: View { // MARK: - Presets private func applyDeepOcean() { - settings.numberOfPoints = 6; settings.viscosity = 0.15; settings.sensitivity = 2.0 - settings.npAmplitude = 0.8; settings.depthOffset = 30; settings.frequencyCutoff = 50 + settings.numberOfPoints = 6; settings.viscosity = 0.12; settings.sensitivity = 0.8 + settings.npAmplitude = 0.75; settings.depthOffset = 30; settings.frequencyCutoff = 50; settings.baseMultiplier = 22 } private func applyReactiveEQ() { - settings.numberOfPoints = 20; settings.viscosity = 0.7; settings.sensitivity = 1.5 - settings.npAmplitude = 0.5; settings.depthOffset = 5; settings.frequencyCutoff = 150 + settings.numberOfPoints = 18; settings.viscosity = 0.6; settings.sensitivity = 1.4 + settings.npAmplitude = 0.5; settings.depthOffset = 5; settings.frequencyCutoff = 120; settings.baseMultiplier = 20 } private func applySubtleAmbient() { - settings.numberOfPoints = 8; settings.viscosity = 0.20; settings.sensitivity = 1.0 - settings.npAmplitude = 0.25; settings.alpha = 0.3; settings.idleAmplitude = 0.05 + settings.numberOfPoints = 8; settings.viscosity = 0.18; settings.sensitivity = 0.7 + settings.npAmplitude = 0.25; settings.alpha = 0.3; settings.idleAmplitude = 0.04; settings.baseMultiplier = 15 } private func applyHeavyMercury() { settings.numberOfPoints = 10; settings.viscosity = 0.12; settings.sensitivity = 2.5 @@ -898,19 +895,19 @@ struct VisualizerSettingsView: View { private func resetDefaults() { settings.enabled = true; settings.nowPlayingEnabled = true; settings.miniPlayerEnabled = true - settings.style = .wave; settings.numberOfPoints = 10; settings.sensitivity = 1.5 - settings.fps = 60; settings.realAudioAnalysis = true; settings.dynamicGainEnabled = false + settings.style = .wave; settings.numberOfPoints = 9 + settings.fps = 60; settings.realAudioAnalysis = true; settings.dynamicGainEnabled = true settings.waveOffsetTop = 0 settings.barSpacing = 5; settings.barCornerRadius = 0; settings.lineThickness = 5 - settings.colorMode = .dynamic; settings.alpha = 0.6; settings.viscosity = 0.25 - // baseMultiplier reduced from 40 → 25 after removing eqBoost (no longer need - // to compensate for the treble under-amplification the boost was masking) - settings.frequencyCutoff = 80; settings.baseMultiplier = 25.0 - settings.depthOffset = 15.0; settings.depthOpacity = 0.2; settings.idleAmplitude = 0.03 + settings.colorMode = .dynamic; settings.alpha = 0.6 + // Tuned for Mitsuha feel: slow liquid viscosity, low sensitivity, moderate gain + settings.viscosity = 0.17; settings.sensitivity = 0.9 + settings.frequencyCutoff = 75; settings.baseMultiplier = 18.0 + settings.depthOffset = 14.0; settings.depthOpacity = 0.18; settings.idleAmplitude = 0.03 settings.waveStrokeThickness = 1.5; settings.nowPlayingHeightPct = 0.50; settings.miniPlayerHeight = 48.0 settings.miniOpacity = 0.5; settings.miniAmplitude = 0.7; settings.miniIdleAmplitude = 0.03 settings.miniDepthOffset = 8.0; settings.miniDepthOpacity = 0.2 - settings.npAmplitude = 0.45; settings.npBaseLift = 130.0 + settings.npAmplitude = 0.50; settings.npBaseLift = 130.0 } } @@ -929,16 +926,16 @@ struct NowPlayingVisSettingsView: View { Text("\(Int(settings.nowPlayingHeightPct * 100))%").foregroundColor(.gray).frame(width: 52, alignment: .trailing) } } - sd("Amplitude", value: $settings.npAmplitude, range: 0.1...1.5, step: 0.05, format: "%.2f") + sd("Amplitude", value: $settings.npAmplitude, range: 0.1...1.0, step: 0.05, format: "%.2f") sd("Base lift (from bottom)", value: $settings.npBaseLift, range: 0...300, step: 5, format: "%.0f pt") sd("Wave offset (top)", value: $settings.waveOffsetTop, range: -100...300, step: 5, format: "%.0f") } header: { Text("LAYOUT & AMPLITUDE") } footer: { Text("Screen height controls how much of the Now Playing screen the visualizer fills. At 50%, it covers the bottom half. At 70%, it reaches behind the album art.\n\nAmplitude controls how tall wave peaks are. At 0.45, peaks reach about halfway. Push to 0.8+ for dramatic waves. Drop to 0.2 for a subtle accent.\n\nBase lift moves the wave baseline up from the very bottom of the screen. At 130 (default), the wave sits above the transport controls. Set to 0 for the absolute bottom edge.\n\nWave offset adds additional vertical shift on top of base lift. Use negative values to push down, positive to push up.") } Section { - sd("Depth offset", value: $settings.depthOffset, range: 0...50, step: 1, format: "%.0f") + sd("Depth offset", value: $settings.depthOffset, range: 0...50, step: 2, format: "%.0f") sd("Depth opacity", value: $settings.depthOpacity, range: 0.0...0.5, step: 0.05, format: "%.2f") - sd("Idle amplitude", value: $settings.idleAmplitude, range: 0.0...0.15, step: 0.005, format: "%.3f") + sd("Idle amplitude", value: $settings.idleAmplitude, range: 0.0...0.10, step: 0.005, format: "%.3f") } header: { Text("DEPTH & IDLE") } footer: { Text("Depth offset controls how far below the main wave the shadow layer is drawn. At 15, there's visible parallax. At 0, they overlap. At 40+, the shadow looks like a distant reflection.\n\nDepth opacity sets how visible the shadow wave is. At 0.2, it's subtle. At 0, invisible.\n\nIdle amplitude is the minimum wave height during quiet moments. Prevents the wave from going completely flat. At 0.03, there's always gentle surface tension.") }