Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

Fix shared waveform state across UI surfaces

authored by

Esteban Uribe and committed by
prompt.ac/@jeffrey
54a430c8 decc2bd9

+56 -5
+9 -1
slab/menuband/Sources/MenuBand/AppDelegate.swift
··· 138 138 self.floatingPlayPalette.refresh() 139 139 self.updateWaveformStrip() 140 140 } 141 + menuBand.onInstrumentVisualChange = { [weak self] in 142 + DispatchQueue.main.async { 143 + guard let self = self else { return } 144 + self.updateIcon() 145 + self.floatingPlayPalette.refresh() 146 + self.waveformStrip.refreshAppearance() 147 + } 148 + } 141 149 menuBand.bootstrap() 142 150 floatingPlayPalette.onDismiss = { [weak self] in 143 151 self?.updateIcon() ··· 743 751 litNotes: menuBand.litNotes, 744 752 enabled: menuBand.midiMode, 745 753 typeMode: menuBand.typeMode, 746 - melodicProgram: menuBand.melodicProgram, 754 + melodicProgram: menuBand.effectiveMelodicProgram, 747 755 hovered: hoveredElement, 748 756 letterAlpha: { [weak self] midi in 749 757 self?.letterAlpha(for: midi) ?? 0
+1 -1
slab/menuband/Sources/MenuBand/FloatingPlayPalette.swift
··· 448 448 .withAlphaComponent(0.55).cgColor 449 449 } else { 450 450 waveformView.setDotMatrix(nil) 451 - let safe = max(0, min(127, Int(menuBand.melodicProgram))) 451 + let safe = max(0, min(127, Int(menuBand.effectiveMelodicProgram))) 452 452 let familyColor = InstrumentListView.colorForProgram(safe) 453 453 waveformView.setBaseColor(familyColor) 454 454 waveformBezel.layer?.borderColor = familyColor
+29 -1
slab/menuband/Sources/MenuBand/MenuBandController.swift
··· 41 41 42 42 var onChange: (() -> Void)? 43 43 var onLitChanged: (() -> Void)? 44 + var onInstrumentVisualChange: (() -> Void)? 44 45 45 46 /// Last MIDI note actually played (mouse tap or keyboard). Used by 46 47 /// the instrument preview / audition path so the "test" note that ··· 131 132 synth.snapshotWaveform(into: &dest) 132 133 } 133 134 135 + /// Multiple surfaces can show the live waveform at once (popover, 136 + /// floating palette, menubar strip). Keep synth capture alive until the 137 + /// last consumer turns itself off, otherwise one view hiding can blank 138 + /// another view that's still visible. 139 + private var waveformCaptureClients = 0 140 + 134 141 func setWaveformCaptureEnabled(_ enabled: Bool) { 135 - synth.setWaveformCaptureEnabled(enabled) 142 + if enabled { 143 + waveformCaptureClients += 1 144 + if waveformCaptureClients == 1 { 145 + synth.setWaveformCaptureEnabled(true) 146 + } 147 + } else { 148 + waveformCaptureClients = max(0, waveformCaptureClients - 1) 149 + if waveformCaptureClients == 0 { 150 + synth.setWaveformCaptureEnabled(false) 151 + } 152 + } 136 153 } 137 154 138 155 // Held preview note for sonic-browse hover over the instrument map. ··· 141 158 // (DAW is the audio path then; we still apply the program change so 142 159 // it's correct when the user toggles MIDI off). 143 160 private var previewNote: UInt8? 161 + private var previewProgram: UInt8? 144 162 /// Pending noteOn dispatched after a setMelodicProgram swap. Held so 145 163 /// a fast hover sequence (cell A → cell B → cell C in <70 ms) cancels 146 164 /// the not-yet-fired note for B before C's load even starts. ··· 162 180 guard let prog = program else { 163 181 // Hover ended — flip back to the committed program so the 164 182 // menubar piano still plays whatever the user actually picked. 183 + previewProgram = nil 165 184 synth.setMelodicProgram(melodicProgram) 185 + onInstrumentVisualChange?() 166 186 return 167 187 } 188 + previewProgram = prog 168 189 synth.setMelodicProgram(prog) 190 + onInstrumentVisualChange?() 169 191 guard !midiMode else { return } 170 192 let note = lastPlayedNote 171 193 previewNote = note ··· 290 312 return UInt8(max(0, min(127, raw))) 291 313 } 292 314 315 + var effectiveMelodicProgram: UInt8 { 316 + previewProgram ?? melodicProgram 317 + } 318 + 293 319 func setMelodicProgram(_ program: UInt8) { 294 320 UserDefaults.standard.set(Int(program), forKey: melodicProgramKey) 321 + previewProgram = nil 295 322 // Picking a GM program implicitly switches us back to the GM 296 323 // backend — the user's "Instrument" pick lives on the GM grid, 297 324 // so committing one means GM is now the active source. 298 325 UserDefaults.standard.set("gm", forKey: instrumentBackendKey) 299 326 synth.setMelodicProgram(program) 300 327 onChange?() 328 + onInstrumentVisualChange?() 301 329 } 302 330 303 331 // MARK: - Instrument backend (GM vs GarageBand)
+6 -1
slab/menuband/Sources/MenuBand/MenuBarWaveformStrip.swift
··· 104 104 positionPanel(panel) 105 105 } 106 106 107 + func refreshAppearance() { 108 + applyAppearanceToVisualizer() 109 + applyWaveformTint() 110 + } 111 + 107 112 // MARK: - Geometry 108 113 109 114 /// Compute the target frame for the strip: directly below the menubar, ··· 313 318 .withAlphaComponent(0.55).cgColor 314 319 } else { 315 320 waveformView.setDotMatrix(nil) 316 - let safe = max(0, min(127, Int(menuBand.melodicProgram))) 321 + let safe = max(0, min(127, Int(menuBand.effectiveMelodicProgram))) 317 322 let familyColor = InstrumentListView.colorForProgram(safe) 318 323 waveformView.setBaseColor(familyColor) 319 324 waveformBezel.layer?.borderColor = familyColor
+11 -1
slab/menuband/Sources/MenuBand/WaveformView.swift
··· 38 38 private var displayLink: CVDisplayLink? 39 39 private let pendingDisplayLock = NSLock() 40 40 private var pendingDisplay = false 41 + /// True only after this view has successfully enabled synth waveform 42 + /// capture for its own live session. `stopLink()` can be reached from 43 + /// multiple lifecycle paths, including ones where no link was started, so 44 + /// we need a per-view lease bit to avoid over-releasing the controller's 45 + /// shared capture refcount. 46 + private var hasCaptureLease = false 41 47 42 48 var isLive: Bool = false { 43 49 didSet { ··· 115 121 let status = CVDisplayLinkStart(link) 116 122 guard status == kCVReturnSuccess else { return } 117 123 menuBand?.setWaveformCaptureEnabled(true) 124 + hasCaptureLease = true 118 125 displayLink = link 119 126 } 120 127 ··· 123 130 CVDisplayLinkStop(link) 124 131 displayLink = nil 125 132 } 126 - menuBand?.setWaveformCaptureEnabled(false) 133 + if hasCaptureLease { 134 + menuBand?.setWaveformCaptureEnabled(false) 135 + hasCaptureLease = false 136 + } 127 137 clearDisplayPending() 128 138 } 129 139