Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

slab/menuband: kick / snare / closed-hat sample pads on the menubar

Three Akai-MPC-style drum pads on the *left* of the piano in the menu
bar status item:

- DrumPad enum: kick (GM 36), snare (GM 38), closed hi-hat (GM 42),
each with a calibrated accent color (warm orange / pink / gold).
- drawDrumPad: rounded-square, vertical gradient (top brighter → bottom
darker), 1 px white sheen at the top edge for the "lit-from-above"
feel, hairline border, single-letter monospaced glyph (K / S / H)
centered. Lit + hover states shift the gradient and brighten the
glyph.
- Layout: pads sized to match white-key width (23 × 21), sit between
the left padding and the piano with a 5 px gap. imageSize +
pianoOriginX + settingsHitRect all derive from the new layout.
drumsAreaWidth collapses to 0 in compact display mode so the
adaptive-sizing fallback still works.
- HitResult gains a `.drum(UInt8)` case. hit() checks drum cells before
falling through to piano + settings.
- AppDelegate.statusClicked: when the initial hit is a drum, fire
startTapNote at velocity 110, sleep 100 ms, stopTapNote. One-shot,
no drag tracking — drums don't glide like piano keys.
- The controller's existing isDrum routing (note < firstMidi → channel
9 → drum sampler) handles the rest, so no audio changes needed.

Site:
- Bump download cache-buster query string to ?v=1fbccdf.

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

+139 -4
+9
slab/menuband/Sources/MenuBand/AppDelegate.swift
··· 272 272 NSSound(named: NSSound.Name("Tink"))?.play() 273 273 showPopover() 274 274 return 275 + case .drum(let drumNote): 276 + // One-shot trigger — drums don't track-drag like piano keys. 277 + // 100 ms hold then release is plenty for the GM drum samples 278 + // to develop their attack. 279 + menuBand.startTapNote(drumNote, velocity: 110, pan: 64) 280 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.10) { [weak menuBand] in 281 + menuBand?.stopTapNote(drumNote) 282 + } 283 + return 275 284 case .note(let n): 276 285 startNote = n 277 286 case .none:
+129 -3
slab/menuband/Sources/MenuBand/KeyboardIconRenderer.swift
··· 86 86 // tall enough to drag across easily 87 87 static let pad: CGFloat = 0.5 88 88 89 + // Drum sample pads on the left of the piano — Akai MPC style: rounded 90 + // squarish pads with a subtle gradient + colored center, sized to 91 + // match white-key width so the row reads as one cohesive instrument. 92 + enum DrumPad: Int, CaseIterable { 93 + case kick = 36 // GM Bass Drum 1 94 + case snare = 38 // GM Acoustic Snare 95 + case closedHat = 42 // GM Closed Hi-Hat 96 + 97 + var midiNote: UInt8 { UInt8(rawValue) } 98 + 99 + /// Pad accent color — warm Akai orange→pink for kick/snare, cooler 100 + /// gold for hi-hat to differentiate at a glance. 101 + var color: NSColor { 102 + switch self { 103 + case .kick: return NSColor(calibratedRed: 1.00, green: 0.42, blue: 0.20, alpha: 1.0) 104 + case .snare: return NSColor(calibratedRed: 1.00, green: 0.30, blue: 0.50, alpha: 1.0) 105 + case .closedHat: return NSColor(calibratedRed: 1.00, green: 0.78, blue: 0.20, alpha: 1.0) 106 + } 107 + } 108 + } 109 + static let drumPads: [DrumPad] = [.kick, .snare, .closedHat] 110 + static let drumW: CGFloat = whiteW // match white-key width 111 + static let drumH: CGFloat = whiteH 112 + static let drumPianoGap: CGFloat = 5.0 // breathing room between pads + piano 113 + 89 114 // Settings — simple monochrome music note that reads like a native 90 115 // status-bar icon. Click → popup menu with TYPE / MIDI / Instrument / 91 116 // About. ··· 96 121 enum HitResult: Equatable { 97 122 case openSettings 98 123 case note(UInt8) 124 + case drum(UInt8) 99 125 } 100 126 101 127 // Letter labels keyed by MIDI note, per layout. The renderer picks ··· 141 167 CGFloat(whiteList().count) * whiteW 142 168 } 143 169 170 + /// Width of the drum pad strip — zero in compact display layout 171 + /// (no piano keys, just the chip), full in any layout that draws keys. 172 + private static var drumsAreaWidth: CGFloat { 173 + displayLayout == .compact ? 0 : CGFloat(drumPads.count) * drumW 174 + } 175 + private static var drumsTrailingGap: CGFloat { 176 + drumsAreaWidth > 0 ? drumPianoGap : 0 177 + } 178 + 144 179 static var imageSize: NSSize { 145 - let totalW = ceil(pad + pianoWidth + settingsGap + settingsW + pad) 180 + let totalW = ceil(pad + drumsAreaWidth + drumsTrailingGap 181 + + pianoWidth + settingsGap + settingsW + pad) 146 182 let totalH = ceil(whiteH + pad * 2) 147 183 return NSSize(width: totalW, height: totalH) 148 184 } 149 185 150 - private static var pianoOriginX: CGFloat { pad } 186 + private static var drumsOriginX: CGFloat { pad } 187 + private static var pianoOriginX: CGFloat { 188 + pad + drumsAreaWidth + drumsTrailingGap 189 + } 190 + 191 + private static func drumRect(at idx: Int) -> NSRect { 192 + let x = drumsOriginX + CGFloat(idx) * drumW 193 + return NSRect(x: x, y: pad, width: drumW, height: drumH) 194 + } 151 195 152 196 /// Settings chip's visual rect IS its hit-test rect — they're identical 153 197 /// so the user gets visual feedback wherever the click lands. ··· 245 289 } 246 290 NSGraphicsContext.restoreGraphicsState() 247 291 292 + // Drum sample pads — Akai MPC style. Drawn after piano so any 293 + // overlap (none expected, but cheap insurance) sits on top. 294 + if drumsAreaWidth > 0 { 295 + for (idx, pad) in drumPads.enumerated() { 296 + let r = drumRect(at: idx) 297 + let isLit = litNotes.contains(pad.midiNote) 298 + let isHover = hovered == .drum(pad.midiNote) 299 + drawDrumPad(in: r, pad: pad, lit: isLit, hovered: isHover) 300 + } 301 + } 302 + 248 303 // Single settings chip — glyph + color reflect MIDI/DAW state. 249 304 // MIDI on → `waveform` tinted accent (signal flowing to DAW); 250 305 // MIDI off → `slider.horizontal.3` in label color (generic). ··· 263 318 // Settings button hit area extends from piano's right edge to the image 264 319 // edge so the entire right-side region is one big click target. 265 320 private static var settingsHitRect: NSRect { 266 - let leftX = pad + pianoWidth 321 + let leftX = pianoOriginX + pianoWidth 267 322 let rightX = imageSize.width 268 323 return NSRect(x: leftX, y: 0, width: rightX - leftX, height: imageSize.height) 269 324 } 270 325 326 + // MARK: - Drum pad drawing 327 + 328 + /// Akai MPC sample-pad look: rounded square, vertical gradient (top 329 + /// brighter → bottom darker), thin border, centered glyph. Lit state 330 + /// flashes the pad accent color full-strength + bright glyph. 331 + private static func drawDrumPad(in rect: NSRect, pad: DrumPad, 332 + lit: Bool, hovered: Bool) { 333 + let inset = rect.insetBy(dx: 1.5, dy: 2.0) 334 + let path = NSBezierPath(roundedRect: inset, xRadius: 3, yRadius: 3) 335 + 336 + let baseColor = pad.color 337 + // Hover/lit shift the pad lightness so feedback reads instantly. 338 + let topColor: NSColor 339 + let botColor: NSColor 340 + if lit { 341 + topColor = baseColor.highlight(withLevel: 0.35) ?? baseColor 342 + botColor = baseColor 343 + } else if hovered { 344 + topColor = baseColor.highlight(withLevel: 0.15) ?? baseColor 345 + botColor = baseColor.shadow(withLevel: 0.10) ?? baseColor 346 + } else { 347 + // Resting: darker so lit feels brighter by contrast. 348 + topColor = baseColor.shadow(withLevel: 0.18) ?? baseColor 349 + botColor = baseColor.shadow(withLevel: 0.45) ?? baseColor 350 + } 351 + NSGradient(starting: topColor, ending: botColor)?.draw(in: path, angle: -90) 352 + 353 + // Inner top sheen — 1 px highlight stripe at the top of the pad 354 + // gives the MPC "lit from above" feel. 355 + let sheen = NSBezierPath(roundedRect: NSRect(x: inset.minX + 0.5, 356 + y: inset.maxY - 1.5, 357 + width: inset.width - 1, 358 + height: 1), 359 + xRadius: 0.5, yRadius: 0.5) 360 + NSColor.white.withAlphaComponent(lit ? 0.55 : 0.30).setFill() 361 + sheen.fill() 362 + 363 + // Border. 364 + NSColor.black.withAlphaComponent(0.45).setStroke() 365 + path.lineWidth = 0.5 366 + path.stroke() 367 + 368 + // Glyph — single-letter abbreviation in monospaced bold, centered. 369 + let letter: String 370 + switch pad { 371 + case .kick: letter = "K" 372 + case .snare: letter = "S" 373 + case .closedHat: letter = "H" 374 + } 375 + let attrs: [NSAttributedString.Key: Any] = [ 376 + .font: NSFont.monospacedSystemFont(ofSize: 9, weight: .heavy), 377 + .foregroundColor: lit 378 + ? NSColor.white 379 + : NSColor.white.withAlphaComponent(0.85), 380 + ] 381 + let str = NSAttributedString(string: letter, attributes: attrs) 382 + let size = str.size() 383 + str.draw(at: NSPoint(x: rect.midX - size.width / 2, 384 + y: rect.midY - size.height / 2 - 0.5)) 385 + } 386 + 271 387 // MARK: - Hit testing 272 388 273 389 static func hit(at point: NSPoint) -> HitResult? { 274 390 if settingsHitRect.contains(point) { return .openSettings } 391 + // Drum pads — checked before piano so the area to the left of the 392 + // keyboard maps to drum hits, not falling through to piano edge 393 + // tolerance. 394 + if drumsAreaWidth > 0 { 395 + for (idx, pad) in drumPads.enumerated() { 396 + if drumRect(at: idx).contains(point) { 397 + return .drum(pad.midiNote) 398 + } 399 + } 400 + } 275 401 let whites = whiteList() 276 402 var whiteIndex: [Int: Int] = [:] 277 403 for (i, m) in whites.enumerated() { whiteIndex[m] = i }
+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=0d61fdb" download> 882 + <a class="aqua" href="https://assets.aesthetic.computer/menuband/Menu-Band-0.1.dmg?v=1fbccdf" download> 883 883 Download 884 884 <small>0.1 · Apple Silicon · 1.1 MB</small> 885 885 </a>