Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

slab/menuband: tiny numeric instrument map + Whistle default + name readout

InstrumentListView rewritten as a non-scrollable 8 × 16 numeric grid:
- 128 cells, one per GM program, each ~28 × 14 px showing just the 3-
digit number ("078") in monospaced. Family-tinted background per row
(16 families = 16 colored stripes top to bottom). Selected cell
flashes the family color full-strength + bold white number.
- No scroll, no names inside cells — the popover gets way smaller and
the whole instrument set is readable at a glance.
- Hover only nudges the row's tint; clicks commit + audition.

Below the grid, a single monospaced readout line shows the selected
program's number + name ("078 Whistle") so the user still sees what
they picked. Updated on syncFromController + handleInstrumentCommit.

Default instrument: Whistle (GM 078) — playful + breathy, more
distinctive than acoustic grand on a fresh install. Fresh-install
default written in bootstrap if the UserDefault isn't set; this user's
pref also flipped to 78 via `defaults write …notepat.melodicProgram`.

About / Crash row:
- Distribution flipped from .fillEqually to .fill, with About hugging
low (expands) and Crash hugging high (sticks to its content). When
there are no crashes, About claims the whole row instead of leaving
empty space on the right.
- About body font bumped 10 → 10.5 with preferredMaxLayoutWidth = the
instrument-map width so the paragraph wraps to the popover width.

Site:
- Bump download cache-buster query string to ?v=237f18c.

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

+117 -239
+77 -219
slab/menuband/Sources/MenuBand/InstrumentMapView.swift
··· 1 1 import AppKit 2 2 3 - /// Two-column sectioned browser for the 128 General MIDI programs. Left 4 - /// column carries the first 8 GM families (Piano … Brass), right column 5 - /// carries the last 8 (Reed … Sound FX). Each family has a colored 6 - /// header + 8 instrument rows beneath it. Click a row → onCommit; the 7 - /// popover hands that to the controller, which sets the program and 8 - /// auditions middle-C through the local synth so the user hears their 9 - /// pick. 3 + /// Tiny numeric flat-map of all 128 General MIDI programs. 8 columns × 16 4 + /// rows, one cell per program — number-only, family-colored background. 5 + /// No scrolling, no names: a glance shows the whole instrument set. 6 + /// Click a cell → onCommit; the popover commits the program and plays a 7 + /// preview note in it. 10 8 /// 11 - /// No hover audio. Hover only nudges the row's background slightly so 12 - /// the cursor's resting target reads at a glance. 9 + /// Replaces the scrollable named-list because the named list ate the 10 + /// popover's vertical real estate. The numeric map is ~224 × 224 px and 11 + /// fits comfortably alongside the rest of the controls. 13 12 final class InstrumentListView: NSView { 14 - static let rowHeight: CGFloat = 18 15 - static let headerHeight: CGFloat = 22 16 - static let stripeWidth: CGFloat = 4 17 - static let sectionPadding: CGFloat = 8 18 - 19 - static let columnWidth: CGFloat = 220 20 - static let columnGap: CGFloat = 8 21 - static let totalColumns = 2 22 - static let familiesPerColumn = 8 // 16 families / 2 columns 23 - static let programsPerFamily = 8 24 - 25 - static let sectionHeight: CGFloat = 26 - headerHeight + CGFloat(programsPerFamily) * rowHeight + sectionPadding 27 - 28 - static let preferredWidth: CGFloat = 29 - CGFloat(totalColumns) * columnWidth + CGFloat(totalColumns - 1) * columnGap 13 + static let cols = 8 14 + static let rows = 16 15 + static let cellW: CGFloat = 28 16 + static let cellH: CGFloat = 14 30 17 31 - static let intrinsicHeight: CGFloat = CGFloat(familiesPerColumn) * sectionHeight 18 + static let preferredWidth: CGFloat = cellW * CGFloat(cols) // 224 19 + static let preferredHeight: CGFloat = cellH * CGFloat(rows) // 224 32 20 33 21 var selectedProgram: UInt8 = 0 { didSet { needsDisplay = true } } 34 22 private(set) var hoveredProgram: UInt8? 35 - 36 - /// Only `onCommit` is exposed — the design dropped hover audio, so we 37 - /// don't even surface a hover callback. The view tracks hover for the 38 - /// background highlight only. 39 23 var onCommit: ((Int) -> Void)? 40 24 41 25 private var trackingArea: NSTrackingArea? 42 26 43 - /// Rows top-down (y=0 → topmost row), matching reading order. 44 - override var isFlipped: Bool { true } 27 + override var isFlipped: Bool { true } // top-down rows, reading order 45 28 46 29 override init(frame frameRect: NSRect) { 47 30 super.init(frame: frameRect) 48 - setFrameSize(NSSize(width: Self.preferredWidth, height: Self.intrinsicHeight)) 31 + setFrameSize(NSSize(width: Self.preferredWidth, height: Self.preferredHeight)) 49 32 } 50 33 required init?(coder: NSCoder) { fatalError() } 51 34 52 35 override var intrinsicContentSize: NSSize { 53 - NSSize(width: Self.preferredWidth, height: Self.intrinsicHeight) 36 + NSSize(width: Self.preferredWidth, height: Self.preferredHeight) 54 37 } 55 38 56 39 override func updateTrackingAreas() { ··· 66 49 trackingArea = ta 67 50 } 68 51 69 - // MARK: - Layout helpers 70 - 71 - /// Origin x of the given column (0 or 1). 72 - private func columnX(_ col: Int) -> CGFloat { 73 - CGFloat(col) * (Self.columnWidth + Self.columnGap) 74 - } 75 - 76 - /// Origin y (top-down) of family index `i` within its column. 77 - private func sectionY(_ familyInColumn: Int) -> CGFloat { 78 - CGFloat(familyInColumn) * Self.sectionHeight 79 - } 80 - 81 - private func rowRect(programInFamily: Int, familyInColumn: Int, column: Int) -> NSRect { 82 - let x = columnX(column) + Self.stripeWidth 83 - let y = sectionY(familyInColumn) + Self.headerHeight + CGFloat(programInFamily) * Self.rowHeight 84 - return NSRect(x: x, y: y, 85 - width: Self.columnWidth - Self.stripeWidth, 86 - height: Self.rowHeight) 87 - } 88 - 89 - private func headerRect(familyInColumn: Int, column: Int) -> NSRect { 90 - NSRect(x: columnX(column), 91 - y: sectionY(familyInColumn), 92 - width: Self.columnWidth, 93 - height: Self.headerHeight) 94 - } 52 + // MARK: - Geometry 95 53 96 - private func stripeRect(familyInColumn: Int, column: Int) -> NSRect { 97 - NSRect(x: columnX(column), 98 - y: sectionY(familyInColumn) + Self.headerHeight, 99 - width: Self.stripeWidth, 100 - height: CGFloat(Self.programsPerFamily) * Self.rowHeight) 54 + private func cellRect(program p: Int) -> NSRect { 55 + let col = p % Self.cols 56 + let row = p / Self.cols 57 + return NSRect(x: CGFloat(col) * Self.cellW, 58 + y: CGFloat(row) * Self.cellH, 59 + width: Self.cellW, 60 + height: Self.cellH) 101 61 } 102 62 103 - /// Given a point in the view, return the program (0..127) under it, 104 - /// or nil if the point falls in a header / column gap / padding area. 105 63 private func program(at point: NSPoint) -> Int? { 106 - // Column. 107 - let stride = Self.columnWidth + Self.columnGap 108 - let col = Int(point.x / stride) 109 - guard col >= 0, col < Self.totalColumns else { return nil } 110 - let xInColumn = point.x - CGFloat(col) * stride 111 - guard xInColumn >= 0, xInColumn <= Self.columnWidth else { return nil } 112 - 113 - // Family within column. 114 - let familyInCol = Int(point.y / Self.sectionHeight) 115 - guard familyInCol >= 0, familyInCol < Self.familiesPerColumn else { return nil } 116 - let yInSection = point.y - CGFloat(familyInCol) * Self.sectionHeight 117 - 118 - // Skip headers + bottom padding. 119 - if yInSection < Self.headerHeight { return nil } 120 - let yInBody = yInSection - Self.headerHeight 121 - let progInFam = Int(yInBody / Self.rowHeight) 122 - guard progInFam >= 0, progInFam < Self.programsPerFamily else { return nil } 123 - 124 - let familyAbs = col * Self.familiesPerColumn + familyInCol 125 - return familyAbs * Self.programsPerFamily + progInFam 64 + guard bounds.contains(point) else { return nil } 65 + let col = Int(point.x / Self.cellW) 66 + let row = Int(point.y / Self.cellH) 67 + guard col >= 0, col < Self.cols, row >= 0, row < Self.rows else { return nil } 68 + let p = row * Self.cols + col 69 + return p < 128 ? p : nil 126 70 } 127 71 128 - // MARK: - Colors 129 - 130 - private func familyColor(_ familyAbs: Int) -> NSColor { 131 - NSColor(hue: CGFloat(familyAbs) / CGFloat(GeneralMIDI.families.count), 132 - saturation: 0.55, brightness: 0.85, alpha: 1.0) 72 + private func familyColor(forProgram p: Int) -> NSColor { 73 + // 16 families × 8 programs each. Each row IS a family (since cols=8). 74 + let famIdx = p / 8 75 + return NSColor(hue: CGFloat(famIdx) / 16.0, 76 + saturation: 0.55, brightness: 0.88, alpha: 1.0) 133 77 } 134 78 135 79 // MARK: - Drawing 136 80 137 - private static let headerAttrs: [NSAttributedString.Key: Any] = [ 138 - .font: NSFont.systemFont(ofSize: 10, weight: .bold), 139 - .foregroundColor: NSColor.labelColor, 140 - .kern: 0.6, 141 - ] 142 - private static let numberAttrs: [NSAttributedString.Key: Any] = [ 143 - .font: NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .regular), 144 - .foregroundColor: NSColor.tertiaryLabelColor, 145 - ] 146 - private static let nameAttrs: [NSAttributedString.Key: Any] = [ 147 - .font: NSFont.systemFont(ofSize: 11, weight: .regular), 148 - .foregroundColor: NSColor.labelColor, 81 + private static let numAttrs: [NSAttributedString.Key: Any] = [ 82 + .font: NSFont.monospacedDigitSystemFont(ofSize: 9, weight: .medium), 83 + .foregroundColor: NSColor.labelColor.withAlphaComponent(0.85), 149 84 ] 150 - private static let nameAttrsSelected: [NSAttributedString.Key: Any] = [ 151 - .font: NSFont.systemFont(ofSize: 11, weight: .semibold), 152 - .foregroundColor: NSColor.labelColor, 85 + private static let numAttrsSelected: [NSAttributedString.Key: Any] = [ 86 + .font: NSFont.monospacedDigitSystemFont(ofSize: 9, weight: .bold), 87 + .foregroundColor: NSColor.white, 153 88 ] 154 89 155 90 override func draw(_ dirtyRect: NSRect) { 156 91 super.draw(dirtyRect) 157 - 158 - for col in 0..<Self.totalColumns { 159 - for familyInCol in 0..<Self.familiesPerColumn { 160 - let familyAbs = col * Self.familiesPerColumn + familyInCol 161 - guard familyAbs < GeneralMIDI.families.count else { continue } 162 - let (familyName, range) = GeneralMIDI.families[familyAbs] 163 - let color = familyColor(familyAbs) 164 - 165 - // Header. 166 - let hRect = headerRect(familyInColumn: familyInCol, column: col) 167 - if hRect.intersects(dirtyRect) { 168 - color.withAlphaComponent(0.18).setFill() 169 - NSBezierPath(rect: hRect).fill() 170 - let titleStr = NSString(string: familyName.uppercased()) 171 - titleStr.draw(at: NSPoint(x: hRect.minX + 8, y: hRect.minY + 5), 172 - withAttributes: Self.headerAttrs) 173 - } 174 - 175 - // Family color stripe spanning the body. 176 - let sRect = stripeRect(familyInColumn: familyInCol, column: col) 177 - if sRect.intersects(dirtyRect) { 178 - color.setFill() 179 - NSBezierPath(rect: sRect).fill() 180 - } 181 - 182 - // Programs. 183 - for progInFam in 0..<Self.programsPerFamily { 184 - let p = range.lowerBound + progInFam 185 - guard p < GeneralMIDI.programNames.count else { continue } 186 - let r = rowRect(programInFamily: progInFam, 187 - familyInColumn: familyInCol, 188 - column: col) 189 - guard r.intersects(dirtyRect) else { continue } 190 - 191 - if hoveredProgram == UInt8(p) { 192 - NSColor.controlAccentColor.withAlphaComponent(0.20).setFill() 193 - NSBezierPath(rect: r).fill() 194 - } else if selectedProgram == UInt8(p) { 195 - NSColor.controlAccentColor.withAlphaComponent(0.12).setFill() 196 - NSBezierPath(rect: r).fill() 197 - } else if progInFam % 2 == 0 { 198 - NSColor.labelColor.withAlphaComponent(0.025).setFill() 199 - NSBezierPath(rect: r).fill() 200 - } 92 + for p in 0..<128 { 93 + let r = cellRect(program: p) 94 + guard r.intersects(dirtyRect) else { continue } 95 + let fam = familyColor(forProgram: p) 201 96 202 - // Program number — three monospace digits, dim. 203 - let num = NSString(format: "%03d", p) 204 - num.draw(at: NSPoint(x: r.minX + 4, y: r.minY + 3), 205 - withAttributes: Self.numberAttrs) 97 + // Background — family-tinted at low opacity so the grid reads 98 + // as 16 rainbow rows. 99 + fam.withAlphaComponent(0.20).setFill() 100 + NSBezierPath(rect: r).fill() 206 101 207 - // Name — truncated to fit column width. 208 - let nameAttrs = (selectedProgram == UInt8(p)) 209 - ? Self.nameAttrsSelected 210 - : Self.nameAttrs 211 - let name = GeneralMIDI.programNames[p] 212 - let nameOriginX = r.minX + 32 213 - let nameMaxWidth = r.maxX - nameOriginX - 4 214 - drawTruncated(name, at: NSPoint(x: nameOriginX, y: r.minY + 2), 215 - maxWidth: nameMaxWidth, attrs: nameAttrs) 216 - } 102 + if selectedProgram == UInt8(p) { 103 + fam.setFill() 104 + NSBezierPath(rect: r).fill() 105 + } else if hoveredProgram == UInt8(p) { 106 + NSColor.controlAccentColor.withAlphaComponent(0.35).setFill() 107 + NSBezierPath(rect: r).fill() 217 108 } 218 - } 219 - } 220 109 221 - private func drawTruncated(_ text: String, at point: NSPoint, 222 - maxWidth: CGFloat, 223 - attrs: [NSAttributedString.Key: Any]) { 224 - var s = text 225 - var attr = NSAttributedString(string: s, attributes: attrs) 226 - if attr.size().width <= maxWidth { 227 - attr.draw(at: point) 228 - return 229 - } 230 - // Tail-truncate. Strip until it fits, then append ellipsis. 231 - let ellipsis = "…" 232 - while !s.isEmpty { 233 - s.removeLast() 234 - attr = NSAttributedString(string: s + ellipsis, attributes: attrs) 235 - if attr.size().width <= maxWidth { break } 110 + // 1px hairline grid. 111 + NSColor.black.withAlphaComponent(0.10).setStroke() 112 + let path = NSBezierPath(rect: r.insetBy(dx: 0.25, dy: 0.25)) 113 + path.lineWidth = 0.5 114 + path.stroke() 115 + 116 + // Program number, centered. 117 + let attrs = (selectedProgram == UInt8(p)) ? Self.numAttrsSelected : Self.numAttrs 118 + let str = NSAttributedString(string: String(format: "%03d", p), attributes: attrs) 119 + let size = str.size() 120 + str.draw(at: NSPoint(x: r.midX - size.width / 2, 121 + y: r.midY - size.height / 2)) 236 122 } 237 - attr.draw(at: point) 238 123 } 239 124 240 125 // MARK: - Mouse ··· 246 131 updateHover(at: convert(event.locationInWindow, from: nil)) 247 132 } 248 133 override func mouseExited(with event: NSEvent) { 249 - if hoveredProgram != nil { 134 + if let prev = hoveredProgram { 250 135 hoveredProgram = nil 251 - needsDisplay = true 136 + setNeedsDisplay(cellRect(program: Int(prev))) 252 137 } 253 138 } 254 139 255 140 private func updateHover(at point: NSPoint) { 256 141 let p = program(at: point).map { UInt8($0) } 257 142 if p != hoveredProgram { 143 + let prev = hoveredProgram 258 144 hoveredProgram = p 259 - needsDisplay = true 145 + if let prev = prev { setNeedsDisplay(cellRect(program: Int(prev))) } 146 + if let p = p { setNeedsDisplay(cellRect(program: Int(p))) } 260 147 } 261 148 } 262 149 263 150 override func mouseDown(with event: NSEvent) { 264 - let pt = convert(event.locationInWindow, from: nil) 265 - let p = program(at: pt) 266 - debugLog("InstrumentListView.mouseDown pt=(\(pt.x),\(pt.y)) program=\(String(describing: p))") 267 - if let p = p { 151 + if let p = program(at: convert(event.locationInWindow, from: nil)) { 268 152 onCommit?(p) 269 153 } 270 154 } 271 155 272 - /// Scroll so the row for `program` is visible. 273 - func scrollProgramIntoView(_ program: UInt8, animated: Bool = false) { 274 - let prog = Int(program) 275 - let familyAbs = prog / Self.programsPerFamily 276 - let progInFam = prog % Self.programsPerFamily 277 - let col = familyAbs / Self.familiesPerColumn 278 - let familyInCol = familyAbs % Self.familiesPerColumn 279 - let r = rowRect(programInFamily: progInFam, 280 - familyInColumn: familyInCol, 281 - column: col) 282 - guard let scroll = enclosingScrollView else { return } 283 - let visible = scroll.documentVisibleRect 284 - if !visible.contains(r) { 285 - let centerY = r.midY - visible.height / 2 286 - let clampedY = max(0, min(bounds.height - visible.height, centerY)) 287 - let newOrigin = NSPoint(x: 0, y: clampedY) 288 - if animated { 289 - NSAnimationContext.runAnimationGroup { ctx in 290 - ctx.duration = 0.25 291 - ctx.allowsImplicitAnimation = true 292 - scroll.contentView.animator().setBoundsOrigin(newOrigin) 293 - scroll.reflectScrolledClipView(scroll.contentView) 294 - } 295 - } else { 296 - scroll.contentView.setBoundsOrigin(newOrigin) 297 - scroll.reflectScrolledClipView(scroll.contentView) 298 - } 299 - } 300 - } 156 + /// No-op kept for API compatibility with the popover (was needed when 157 + /// the list was scrollable; the numeric map shows everything at once). 158 + func scrollProgramIntoView(_ program: UInt8, animated: Bool = false) {} 301 159 }
+6
slab/menuband/Sources/MenuBand/MenuBandController.swift
··· 96 96 // most users hear the synth first; DAW routing is the opt-in path 97 97 // they pick after they know they want it. Existing users' choice 98 98 // is preserved (only the default for first-launch changes). 99 + // Default instrument: Whistle (GM 078) — playful + breathy, more 100 + // distinctive than acoustic grand on a fresh install. Users can 101 + // still pick anything from the popover map. 102 + if UserDefaults.standard.object(forKey: melodicProgramKey) == nil { 103 + UserDefaults.standard.set(78, forKey: melodicProgramKey) 104 + } 99 105 synth.start() 100 106 synth.setMelodicProgram(melodicProgram) 101 107 if UserDefaults.standard.object(forKey: midiModeKey) == nil {
+33 -19
slab/menuband/Sources/MenuBand/MenuBandPopover.swift
··· 63 63 private var midiInlineLabel: NSTextField! 64 64 private var midiSelfTestLabel: NSTextField! // legacy — created but never added to stack 65 65 private var instrumentList: InstrumentListView! 66 + private var instrumentReadout: NSTextField! 66 67 private var octaveStepper: NSStepper! 67 68 private var octaveLabel: NSTextField! 68 69 private var crashStatusLabel: NSTextField! ··· 293 294 stack.addArrangedSubview(instrumentLabel) 294 295 295 296 instrumentList = InstrumentListView() 297 + instrumentList.translatesAutoresizingMaskIntoConstraints = false 296 298 instrumentList.onCommit = { [weak self] prog in 297 299 self?.handleInstrumentCommit(prog) 298 300 } 299 - let scroll = NSScrollView() 300 - scroll.translatesAutoresizingMaskIntoConstraints = false 301 - scroll.hasVerticalScroller = true 302 - scroll.hasHorizontalScroller = false 303 - scroll.autohidesScrollers = true 304 - scroll.borderType = .lineBorder 305 - scroll.scrollerStyle = .overlay 306 - scroll.documentView = instrumentList 307 - stack.addArrangedSubview(scroll) 308 - scroll.widthAnchor.constraint(equalToConstant: InstrumentListView.preferredWidth).isActive = true 309 - scroll.heightAnchor.constraint(equalToConstant: 180).isActive = true 301 + stack.addArrangedSubview(instrumentList) 302 + instrumentList.widthAnchor.constraint(equalToConstant: InstrumentListView.preferredWidth).isActive = true 303 + instrumentList.heightAnchor.constraint(equalToConstant: InstrumentListView.preferredHeight).isActive = true 304 + 305 + // Readout for the selected program — "078 Whistle" — sits right 306 + // below the grid since the cells themselves only show numbers. 307 + instrumentReadout = NSTextField(labelWithString: "") 308 + instrumentReadout.font = NSFont.monospacedSystemFont(ofSize: 11, weight: .medium) 309 + instrumentReadout.textColor = .labelColor 310 + instrumentReadout.lineBreakMode = .byTruncatingTail 311 + stack.addArrangedSubview(instrumentReadout) 310 312 311 313 stack.addArrangedSubview(makeSeparator()) 312 314 313 - // About + Crash logs in a side-by-side row to save vertical space. 314 - // Each takes half the popover width. Crash column is hidden when 315 - // there are no recent reports, leaving About full-width. 315 + // About + Crash logs in a side-by-side row. About has low hugging 316 + // so it expands when the crash column is hidden (no reports) — 317 + // takes the whole row instead of leaving negative space on the 318 + // right. With reports present, the crash column claims its 319 + // intrinsic content width and About fills what's left. 316 320 let aboutCrashRow = NSStackView() 317 321 aboutCrashRow.orientation = .horizontal 318 322 aboutCrashRow.alignment = .top 319 - aboutCrashRow.distribution = .fillEqually 323 + aboutCrashRow.distribution = .fill 320 324 aboutCrashRow.spacing = 12 321 325 322 326 let aboutCol = NSStackView() ··· 329 333 let aboutBody = NSTextField(wrappingLabelWithString: 330 334 "A political project to bring the built-in macOS instruments — " + 331 335 "the ones GarageBand uses — into the menu bar. Free + open source.") 332 - aboutBody.font = NSFont.systemFont(ofSize: 10) 336 + aboutBody.font = NSFont.systemFont(ofSize: 10.5) 333 337 aboutBody.textColor = .secondaryLabelColor 334 338 aboutBody.maximumNumberOfLines = 0 335 - aboutBody.preferredMaxLayoutWidth = 200 339 + aboutBody.preferredMaxLayoutWidth = InstrumentListView.preferredWidth 340 + aboutCol.setContentHuggingPriority(.defaultLow, for: .horizontal) 336 341 aboutCol.addArrangedSubview(aboutTitle) 337 342 aboutCol.addArrangedSubview(aboutBody) 338 343 let linksRow = NSStackView() ··· 362 367 crashHintLabel.font = NSFont.systemFont(ofSize: 10) 363 368 crashHintLabel.textColor = .secondaryLabelColor 364 369 crashHintLabel.maximumNumberOfLines = 0 365 - crashHintLabel.preferredMaxLayoutWidth = 200 370 + crashHintLabel.preferredMaxLayoutWidth = 130 371 + crashCol.setContentHuggingPriority(.defaultHigh, for: .horizontal) 366 372 crashSendButton = NSButton(title: "Send crash reports", 367 373 target: self, 368 374 action: #selector(sendCrashLogs(_:))) ··· 428 434 inputSegmented.selectedSegment = inputModeSegment(typeMode: n.typeMode, 429 435 keymap: n.keymap) 430 436 instrumentList.selectedProgram = n.melodicProgram 431 - instrumentList.scrollProgramIntoView(n.melodicProgram) 437 + updateInstrumentReadout(program: n.melodicProgram) 432 438 updateSelfTestLabel(state: n.midiMode ? n.midiSelfTest : .unknown) 433 439 refreshCrashStatus() 434 440 refreshUpdateBanner() ··· 446 452 self.updateSelfTestLabel(state: nn.midiMode ? nn.midiSelfTest : .unknown) 447 453 } 448 454 } 455 + } 456 + 457 + /// Format the readout below the numeric grid as "078 Whistle". 458 + private func updateInstrumentReadout(program: UInt8) { 459 + let safe = max(0, min(127, Int(program))) 460 + let name = GeneralMIDI.programNames[safe] 461 + instrumentReadout.stringValue = String(format: "%03d %@", safe, name) 449 462 } 450 463 451 464 /// Reflect the MIDI loopback self-test status as the inline "MIDI" ··· 625 638 guard let m = menuBand else { return } 626 639 m.setMelodicProgram(UInt8(program)) 627 640 instrumentList.selectedProgram = UInt8(program) 641 + updateInstrumentReadout(program: UInt8(program)) 628 642 debugLog("instrument commit prog=\(program)") 629 643 // setMelodicProgram → loadSoundBankInstrument is synchronous on the 630 644 // calling thread, but AVAudioUnitSampler briefly drops scheduled
+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=b83144b" download> 882 + <a class="aqua" href="https://assets.aesthetic.computer/menuband/Menu-Band-0.1.dmg?v=237f18c" download> 883 883 Download 884 884 <small>0.1 · Apple Silicon · 1.1 MB</small> 885 885 </a>