···41414242 var onChange: (() -> Void)?
4343 var onLitChanged: (() -> Void)?
4444+ var onInstrumentVisualChange: (() -> Void)?
44454546 /// Last MIDI note actually played (mouse tap or keyboard). Used by
4647 /// the instrument preview / audition path so the "test" note that
···131132 synth.snapshotWaveform(into: &dest)
132133 }
133134135135+ /// Multiple surfaces can show the live waveform at once (popover,
136136+ /// floating palette, menubar strip). Keep synth capture alive until the
137137+ /// last consumer turns itself off, otherwise one view hiding can blank
138138+ /// another view that's still visible.
139139+ private var waveformCaptureClients = 0
140140+134141 func setWaveformCaptureEnabled(_ enabled: Bool) {
135135- synth.setWaveformCaptureEnabled(enabled)
142142+ if enabled {
143143+ waveformCaptureClients += 1
144144+ if waveformCaptureClients == 1 {
145145+ synth.setWaveformCaptureEnabled(true)
146146+ }
147147+ } else {
148148+ waveformCaptureClients = max(0, waveformCaptureClients - 1)
149149+ if waveformCaptureClients == 0 {
150150+ synth.setWaveformCaptureEnabled(false)
151151+ }
152152+ }
136153 }
137154138155 // Held preview note for sonic-browse hover over the instrument map.
···141158 // (DAW is the audio path then; we still apply the program change so
142159 // it's correct when the user toggles MIDI off).
143160 private var previewNote: UInt8?
161161+ private var previewProgram: UInt8?
144162 /// Pending noteOn dispatched after a setMelodicProgram swap. Held so
145163 /// a fast hover sequence (cell A → cell B → cell C in <70 ms) cancels
146164 /// the not-yet-fired note for B before C's load even starts.
···162180 guard let prog = program else {
163181 // Hover ended — flip back to the committed program so the
164182 // menubar piano still plays whatever the user actually picked.
183183+ previewProgram = nil
165184 synth.setMelodicProgram(melodicProgram)
185185+ onInstrumentVisualChange?()
166186 return
167187 }
188188+ previewProgram = prog
168189 synth.setMelodicProgram(prog)
190190+ onInstrumentVisualChange?()
169191 guard !midiMode else { return }
170192 let note = lastPlayedNote
171193 previewNote = note
···290312 return UInt8(max(0, min(127, raw)))
291313 }
292314315315+ var effectiveMelodicProgram: UInt8 {
316316+ previewProgram ?? melodicProgram
317317+ }
318318+293319 func setMelodicProgram(_ program: UInt8) {
294320 UserDefaults.standard.set(Int(program), forKey: melodicProgramKey)
321321+ previewProgram = nil
295322 // Picking a GM program implicitly switches us back to the GM
296323 // backend — the user's "Instrument" pick lives on the GM grid,
297324 // so committing one means GM is now the active source.
298325 UserDefaults.standard.set("gm", forKey: instrumentBackendKey)
299326 synth.setMelodicProgram(program)
300327 onChange?()
328328+ onInstrumentVisualChange?()
301329 }
302330303331 // MARK: - Instrument backend (GM vs GarageBand)
···104104 positionPanel(panel)
105105 }
106106107107+ func refreshAppearance() {
108108+ applyAppearanceToVisualizer()
109109+ applyWaveformTint()
110110+ }
111111+107112 // MARK: - Geometry
108113109114 /// Compute the target frame for the strip: directly below the menubar,
···313318 .withAlphaComponent(0.55).cgColor
314319 } else {
315320 waveformView.setDotMatrix(nil)
316316- let safe = max(0, min(127, Int(menuBand.melodicProgram)))
321321+ let safe = max(0, min(127, Int(menuBand.effectiveMelodicProgram)))
317322 let familyColor = InstrumentListView.colorForProgram(safe)
318323 waveformView.setBaseColor(familyColor)
319324 waveformBezel.layer?.borderColor = familyColor
+11-1
slab/menuband/Sources/MenuBand/WaveformView.swift
···3838 private var displayLink: CVDisplayLink?
3939 private let pendingDisplayLock = NSLock()
4040 private var pendingDisplay = false
4141+ /// True only after this view has successfully enabled synth waveform
4242+ /// capture for its own live session. `stopLink()` can be reached from
4343+ /// multiple lifecycle paths, including ones where no link was started, so
4444+ /// we need a per-view lease bit to avoid over-releasing the controller's
4545+ /// shared capture refcount.
4646+ private var hasCaptureLease = false
41474248 var isLive: Bool = false {
4349 didSet {
···115121 let status = CVDisplayLinkStart(link)
116122 guard status == kCVReturnSuccess else { return }
117123 menuBand?.setWaveformCaptureEnabled(true)
124124+ hasCaptureLease = true
118125 displayLink = link
119126 }
120127···123130 CVDisplayLinkStop(link)
124131 displayLink = nil
125132 }
126126- menuBand?.setWaveformCaptureEnabled(false)
133133+ if hasCaptureLease {
134134+ menuBand?.setWaveformCaptureEnabled(false)
135135+ hasCaptureLease = false
136136+ }
127137 clearDisplayPending()
128138 }
129139