Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

slab/menuband: lean bottom-anchored bar visualizer + crash count filter

Visualizer rewritten as ground-anchored monochrome bars:
- Single CAShapeLayer with one combined CGPath of 32 bar rects, single
systemTeal fill. No gradient mask, no glow, no peak-hold decay,
no hue rotation.
- Bottom-anchored (NSView default coords, y=0 at bottom) so bars grow
upward from a flat floor — the more familiar "VU meter" / spectrum
look the user wanted.
- Plain Timer at 60 Hz, added to RunLoop.main with .common mode so
it fires at full rate even while the user interacts with menus
(without .common the timer was stalling to ~12 Hz).
- Per-bar peak from a 512-frame window directly mapped to height with
a 2.5× gain. No smoothing — bars react instantly to attacks.
- Audio tap buffer dropped 512 → 256 frames (5.8 ms at 44.1 kHz) for
fresher data with no perceived latency.

Crash count filter:
- CrashLogReader.recentLogs() now filters to .ips files modified after
the running executable's mtime. install.sh `cp`s the new binary on
every reinstall, so this advances per build — pre-fix crashes from
earlier dev iterations stop counting against the running build's
reputation. Prevents the "25 recent crashes" surprise.

Site:
- Bump download cache-buster query string to ?v=4b845f9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+62 -102
+18 -2
slab/menuband/Sources/MenuBand/CrashLogReader.swift
··· 10 10 11 11 static let lithEndpoint = URL(string: "https://aesthetic.computer/menuband-logs")! 12 12 13 - /// All MenuBand crash reports in the diagnostic dir, newest-modified first. 13 + /// MenuBand crash reports from the *currently-installed* build, newest 14 + /// first. Filter cutoff = the executable's mtime, which install.sh 15 + /// updates on every reinstall — so crashes from older, since-fixed 16 + /// builds (very common during heavy dev iteration) don't pollute 17 + /// the count surfaced to the user. 14 18 static func recentLogs() -> [URL] { 15 19 let dirURL = URL(fileURLWithPath: directoryPath) 16 20 guard let entries = try? FileManager.default.contentsOfDirectory( ··· 20 24 ) else { 21 25 return [] 22 26 } 27 + let installedAt = bundleInstalledAt() 23 28 return entries 24 29 .filter { url in 25 30 let name = url.lastPathComponent 26 31 guard name.hasPrefix("MenuBand-") else { return false } 27 32 let ext = url.pathExtension 28 - return ext == "ips" || ext == "crash" 33 + guard ext == "ips" || ext == "crash" else { return false } 34 + let mtime = (try? url.resourceValues(forKeys: [.contentModificationDateKey]))? 35 + .contentModificationDate ?? .distantPast 36 + return mtime > installedAt 29 37 } 30 38 .sorted { (a, b) in 31 39 let ad = (try? a.resourceValues(forKeys: [.contentModificationDateKey]))? ··· 34 42 .contentModificationDate ?? .distantPast 35 43 return ad > bd 36 44 } 45 + } 46 + 47 + /// mtime of the running executable. install.sh `cp`s the new binary 48 + /// into place on every reinstall, so this advances with each build. 49 + private static func bundleInstalledAt() -> Date { 50 + guard let exe = Bundle.main.executableURL else { return .distantPast } 51 + return (try? exe.resourceValues(forKeys: [.contentModificationDateKey]))? 52 + .contentModificationDate ?? .distantPast 37 53 } 38 54 39 55 /// Upload one crash report to lith. Calls back on the main thread.
+3 -1
slab/menuband/Sources/MenuBand/MenuBandSynth.swift
··· 55 55 private func installWaveformTap() { 56 56 let mixer = engine.mainMixerNode 57 57 let format = mixer.outputFormat(forBus: 0) 58 - mixer.installTap(onBus: 0, bufferSize: 512, format: format) { [weak self] buffer, _ in 58 + // 256 frames ≈ 5.8 ms at 44.1 kHz — small buffer = fresher samples 59 + // for the visualizer without burning the audio thread. 60 + mixer.installTap(onBus: 0, bufferSize: 256, format: format) { [weak self] buffer, _ in 59 61 self?.ingestWaveformBuffer(buffer) 60 62 } 61 63 }
+40 -98
slab/menuband/Sources/MenuBand/WaveformView.swift
··· 1 1 import AppKit 2 2 3 - /// Live audio visualizer for the local synth output. Single antialiased 4 - /// line, stroked with a horizontal rainbow gradient (CAGradientLayer + 5 - /// CAShapeLayer mask) so it reads as a "visualizer" rather than a flat 6 - /// scope. Hidden when MIDI mode is on (DAW handles audio there; the 7 - /// local mixer is silent). 8 - /// 9 - /// Layer-based draw path: the path is updated on a 60 Hz timer with 10 - /// implicit animations off, so each frame snaps cleanly to the new 11 - /// shape with no tweening. Cheap enough to keep the popover snappy. 3 + /// Bottom-anchored audio bars. Single CAShapeLayer with one combined path 4 + /// of all bar rects, single monochrome fill — no gradient, no glow, no 5 + /// peak-hold decay. Plain 60 Hz Timer on `.common` runloop drives the 6 + /// path update. Designed to be as cheap as possible per frame: read 7 + /// samples, compute 32 peaks, build path, swap path. Hidden when MIDI 8 + /// mode is on (synth silent there). 12 9 final class WaveformView: NSView { 13 10 weak var menuBand: MenuBandController? 14 11 15 - private static let sampleCount = 512 16 - private var samples = [Float](repeating: 0, count: sampleCount) 12 + private static let barCount = 32 13 + private static let barGap: CGFloat = 2 14 + private static let snapshotSize = 512 // samples we look at per frame 17 15 18 - private let gradientLayer = CAGradientLayer() 19 - private let lineMask = CAShapeLayer() 20 - private let glowLayer = CAShapeLayer() 21 - 16 + private var samples = [Float](repeating: 0, count: snapshotSize) 17 + private let barLayer = CAShapeLayer() 22 18 private var refreshTimer: Timer? 23 - private var hue: CGFloat = 0 // slowly rotated each frame for shimmer 24 19 25 20 var isLive: Bool = false { 26 21 didSet { ··· 33 28 wantsLayer = true 34 29 layer?.backgroundColor = NSColor.black.withAlphaComponent(0.92).cgColor 35 30 layer?.cornerRadius = 8 36 - layer?.borderWidth = 0.5 37 - layer?.borderColor = NSColor.white.withAlphaComponent(0.10).cgColor 38 - 39 - // A soft glow under the line — lower opacity, wider stroke. 40 - glowLayer.fillColor = nil 41 - glowLayer.lineWidth = 4.5 42 - glowLayer.lineJoin = .round 43 - glowLayer.lineCap = .round 44 - glowLayer.strokeColor = NSColor.white.withAlphaComponent(0.20).cgColor 45 - glowLayer.shadowColor = NSColor.systemTeal.cgColor 46 - glowLayer.shadowOpacity = 0.55 47 - glowLayer.shadowRadius = 6 48 - glowLayer.shadowOffset = .zero 49 - layer?.addSublayer(glowLayer) 50 - 51 - // Sharp foreground line, masked by the rainbow gradient. 52 - lineMask.fillColor = nil 53 - lineMask.lineWidth = 1.5 54 - lineMask.lineJoin = .round 55 - lineMask.lineCap = .round 56 - lineMask.strokeColor = NSColor.black.cgColor 57 - 58 - gradientLayer.colors = [ 59 - NSColor.systemPink.cgColor, 60 - NSColor.systemOrange.cgColor, 61 - NSColor.systemYellow.cgColor, 62 - NSColor.systemGreen.cgColor, 63 - NSColor.systemTeal.cgColor, 64 - NSColor.systemPurple.cgColor, 65 - ] 66 - gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5) 67 - gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5) 68 - gradientLayer.mask = lineMask 69 - layer?.addSublayer(gradientLayer) 31 + barLayer.fillColor = NSColor.systemTeal.cgColor 32 + barLayer.strokeColor = nil 33 + barLayer.actions = ["path": NSNull()] // no implicit anim on path 34 + layer?.addSublayer(barLayer) 70 35 } 71 36 required init?(coder: NSCoder) { fatalError() } 72 37 73 - override var wantsUpdateLayer: Bool { true } 74 - 75 38 override func layout() { 76 39 super.layout() 77 - gradientLayer.frame = bounds 78 - glowLayer.frame = bounds 79 - lineMask.frame = bounds 40 + barLayer.frame = bounds 80 41 } 42 + 43 + deinit { stopTimer() } 81 44 82 45 private func startTimer() { 83 46 stopTimer() 84 - // 60 Hz. Path update is GPU-accelerated; CPU cost is the sample 85 - // walk + path build (512 line segments) — negligible. 86 - refreshTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 60.0, repeats: true) { [weak self] _ in 47 + let t = Timer(timeInterval: 1.0 / 60.0, repeats: true) { [weak self] _ in 87 48 self?.tick() 88 49 } 89 - if let t = refreshTimer { RunLoop.main.add(t, forMode: .common) } 50 + // .common so it fires while the user is interacting with menus, 51 + // dragging, etc. Without this, the timer can stall to ~12 Hz. 52 + RunLoop.main.add(t, forMode: .common) 53 + refreshTimer = t 90 54 } 91 55 92 56 private func stopTimer() { ··· 106 70 let w = bounds.width 107 71 let h = bounds.height 108 72 guard w > 0, h > 0 else { return } 109 - let midY = h * 0.5 110 73 111 - // Auto-gain: scale to ~85% of half-height by peak. Floor the peak 112 - // so silence doesn't blow up to look like noise. 113 - var peak: Float = 0.05 114 - for s in samples { 115 - let a = abs(s) 116 - if a > peak { peak = a } 117 - } 118 - let scale = CGFloat(0.88) / CGFloat(min(max(peak, 0.05), 1.0)) 74 + let n = Self.barCount 75 + let chunkSize = samples.count / n 76 + let barW = (w - Self.barGap * CGFloat(n - 1)) / CGFloat(n) 77 + let stride = barW + Self.barGap 78 + let gain: CGFloat = 2.5 // typical synth peak ~0.3–0.4, push toward full height 119 79 120 80 let path = CGMutablePath() 121 - let n = samples.count 122 - guard n > 1 else { return } 123 - let step = w / CGFloat(n - 1) 124 - for i in 0..<n { 125 - let x = CGFloat(i) * step 126 - let y = midY - CGFloat(samples[i]) * midY * scale 127 - if i == 0 { 128 - path.move(to: CGPoint(x: x, y: y)) 129 - } else { 130 - path.addLine(to: CGPoint(x: x, y: y)) 81 + for b in 0..<n { 82 + var peak: Float = 0 83 + let base = b * chunkSize 84 + for i in 0..<chunkSize { 85 + let a = abs(samples[base + i]) 86 + if a > peak { peak = a } 131 87 } 132 - } 133 - 134 - // Rotate the gradient hue slowly so quiet content still looks alive. 135 - hue += 0.0035 136 - if hue > 1.0 { hue -= 1.0 } 137 - let h0 = hue 138 - let h1 = (hue + 0.16).truncatingRemainder(dividingBy: 1.0) 139 - let h2 = (hue + 0.33).truncatingRemainder(dividingBy: 1.0) 140 - let h3 = (hue + 0.50).truncatingRemainder(dividingBy: 1.0) 141 - let h4 = (hue + 0.66).truncatingRemainder(dividingBy: 1.0) 142 - let h5 = (hue + 0.83).truncatingRemainder(dividingBy: 1.0) 143 - let cgcolor = { (hue: CGFloat) -> CGColor in 144 - NSColor(hue: hue, saturation: 0.85, brightness: 1.0, alpha: 1.0).cgColor 88 + let lvl = Swift.min(1.0, CGFloat(peak) * gain) 89 + // Bottom-anchored: y=0 is bottom (NSView default coord space). 90 + let bh = Swift.max(1.5, lvl * h) 91 + let bx = CGFloat(b) * stride 92 + path.addRect(CGRect(x: bx, y: 0, width: barW, height: bh)) 145 93 } 146 94 147 - CATransaction.begin() 148 - CATransaction.setDisableActions(true) 149 - gradientLayer.colors = [cgcolor(h0), cgcolor(h1), cgcolor(h2), 150 - cgcolor(h3), cgcolor(h4), cgcolor(h5)] 151 - lineMask.path = path 152 - glowLayer.path = path 153 - CATransaction.commit() 95 + barLayer.path = path 154 96 } 155 97 }
+1 -1
system/public/menuband/index.html
··· 879 879 <p class="tagline">Built-in macOS instruments, in the menu bar.</p> 880 880 881 881 <div class="button-row"> 882 - <a class="aqua" href="https://assets.aesthetic.computer/menuband/Menu-Band-0.1.dmg?v=8fbda61" download> 882 + <a class="aqua" href="https://assets.aesthetic.computer/menuband/Menu-Band-0.1.dmg?v=4b845f9" download> 883 883 Download 884 884 <small>0.1 · Apple Silicon · 1.1 MB</small> 885 885 </a>