Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

slab/menuband: press-gated rollover + GM bank-select for MIDISynth

- Instrument grid: rollover (visual lit + preview audio) is now press-
gated. Passive mouse-over does nothing. Press anywhere on the grid
and drag to browse cells; release stops the preview note and commits
the cell under the cursor.
- MIDISynth program switches send a proper GM bank-select pair (CC0 =
bank MSB 0x79, CC32 = bank LSB 0) before the Program Change. Without
the bank-select, raw PCs were landing in an arbitrary bank state and
rollover sounds weren't actually changing program. The
EnablePreload-based 128-program preload sweep is gone — it was the
thing preventing later PCs from switching the active program. Bank
loads are lazy now (~5–20 ms warmup per new program after the disk
cache is hot).

+44 -41
+18 -8
slab/menuband/Sources/MenuBand/InstrumentMapView.swift
··· 128 128 129 129 // MARK: - Mouse 130 130 131 + // Hover preview is press-gated: passive mouse-over does NOT light cells 132 + // or trigger preview audio. The user has to mouseDown first; while held, 133 + // dragging across cells lights/sounds each one and unlights it on the 134 + // way out. mouseUp stops the sound and commits the cell under the cursor. 135 + private var dragging = false 136 + 131 137 override func mouseEntered(with event: NSEvent) { 138 + guard dragging else { return } 132 139 updateHover(at: convert(event.locationInWindow, from: nil)) 133 140 } 134 141 override func mouseMoved(with event: NSEvent) { 142 + guard dragging else { return } 135 143 updateHover(at: convert(event.locationInWindow, from: nil)) 136 144 } 137 145 override func mouseExited(with event: NSEvent) { 146 + guard dragging else { return } 138 147 if let prev = hoveredProgram { 139 148 hoveredProgram = nil 140 149 setNeedsDisplay(cellRect(program: Int(prev))) ··· 153 162 } 154 163 } 155 164 156 - // mouseDown → mouseDragged → mouseUp lets the user click-drag across 157 - // cells to sonically browse: every cell crossed during the drag fires 158 - // onHover (preview note); release commits the cell under the cursor. 159 - private var dragging = false 160 - 161 165 override func mouseDown(with event: NSEvent) { 162 166 dragging = true 163 167 let pt = convert(event.locationInWindow, from: nil) 164 168 if let p = program(at: pt) { 165 - // Treat the press itself as a hover-into-this-cell so the 166 - // preview note starts immediately on click, not only on 167 - // dragging away. 169 + // Treat the press as a hover-into-this-cell so the preview note 170 + // and lit highlight start immediately on click. 168 171 if hoveredProgram != UInt8(p) { 169 172 let prev = hoveredProgram 170 173 hoveredProgram = UInt8(p) ··· 184 187 guard dragging else { return } 185 188 dragging = false 186 189 let pt = convert(event.locationInWindow, from: nil) 190 + // Release stops the preview note + clears the lit highlight, then 191 + // commits the cell that was under the cursor at release time. The 192 + // commit path will re-light the cell as the *selected* program. 193 + let prevHover = hoveredProgram 194 + hoveredProgram = nil 195 + if let prev = prevHover { setNeedsDisplay(cellRect(program: Int(prev))) } 196 + onHover?(nil) 187 197 if let p = program(at: pt) { 188 198 onCommit?(p) 189 199 }
+26 -33
slab/menuband/Sources/MenuBand/MenuBandSynth.swift
··· 129 129 engine.attach(avUnit) 130 130 engine.connect(avUnit, to: engine.mainMixerNode, format: nil) 131 131 132 - // 3. Enable preload mode, send Program Change for every program we 133 - // want to use, then disable preload. Each PC during preload mode 134 - // triggers a synchronous bank load for that (bank, program); after 135 - // preload is disabled, subsequent PCs become instant switches. 136 - setMIDISynthPreload(au, enable: true) 137 - for p: UInt8 in 0...127 { 138 - sendMIDIEvent(au, status: 0xC0, data1: p) // PC ch 0 139 - } 140 - // Drum kit on channel 9 — bankMSB 0x78 = GM Percussion. Send a 141 - // bank-select pair followed by PC 0 to land on the standard kit. 142 - sendMIDIEvent(au, status: 0xB9, data1: 0, data2: 0x78) // CC0 = MSB 143 - sendMIDIEvent(au, status: 0xB9, data1: 32, data2: 0) // CC32 = LSB 144 - sendMIDIEvent(au, status: 0xC9, data1: 0) // PC ch 9 145 - setMIDISynthPreload(au, enable: false) 146 - 147 - // 4. Reset channel 0 to the user's last-picked program. The earlier 148 - // sweep left it on program 127 — we want it to match what the 149 - // user expects. 150 - sendMIDIEvent(au, status: 0xC0, data1: currentMelodicProgram) 132 + // 3. Set channel 0 to GM Melodic bank + the user's current program. 133 + // Channel 9 to GM Percussion + standard kit. We let MIDISynth 134 + // lazy-load programs on first use rather than preloading the 135 + // full bank — the preload-mode dance was preventing later PC 136 + // messages from actually switching the active program. Lazy 137 + // load gives a few ms of disk warmup per new program but 138 + // program switches Just Work after that. 139 + selectMelodicProgram(au, program: currentMelodicProgram) 140 + selectDrumKit(au) 151 141 152 142 midiSynth = avUnit 153 143 midiSynthReady = true ··· 160 150 // (Leaving the nodes attached but unrouted is safe.) 161 151 } 162 152 163 - private func setMIDISynthPreload(_ au: AudioUnit, enable: Bool) { 164 - var flag: UInt32 = enable ? 1 : 0 165 - let status = AudioUnitSetProperty( 166 - au, 167 - AudioUnitPropertyID(kAUMIDISynthProperty_EnablePreload), 168 - kAudioUnitScope_Global, 169 - 0, 170 - &flag, 171 - UInt32(MemoryLayout<UInt32>.size) 172 - ) 173 - if status != noErr { 174 - NSLog("MenuBand: MIDISynth EnablePreload(\(enable)) status=\(status)") 175 - } 153 + /// Select a melodic program on channel 0 of the MIDISynth via a proper 154 + /// GM bank-select + Program Change triplet. CC0 = bank MSB (0x79 for the 155 + /// GM Melodic bank), CC32 = bank LSB (0), then PC = program. Without 156 + /// the bank-select pair, raw PC messages may end up routing into a 157 + /// different bank and produce silent/wrong-program output. 158 + private func selectMelodicProgram(_ au: AudioUnit, program: UInt8) { 159 + sendMIDIEvent(au, status: 0xB0, data1: 0, data2: 0x79) // CC0 bank MSB 160 + sendMIDIEvent(au, status: 0xB0, data1: 32, data2: 0x00) // CC32 bank LSB 161 + sendMIDIEvent(au, status: 0xC0, data1: program) // PC program 162 + } 163 + 164 + /// Select the standard GM drum kit on channel 9 (bank MSB 0x78). 165 + private func selectDrumKit(_ au: AudioUnit) { 166 + sendMIDIEvent(au, status: 0xB9, data1: 0, data2: 0x78) // CC0 bank MSB 167 + sendMIDIEvent(au, status: 0xB9, data1: 32, data2: 0x00) // CC32 bank LSB 168 + sendMIDIEvent(au, status: 0xC9, data1: 0) // PC kit 0 176 169 } 177 170 178 171 @inline(__always) ··· 266 259 func setMelodicProgram(_ program: UInt8) { 267 260 currentMelodicProgram = program 268 261 if midiSynthReady, let au = midiSynth?.audioUnit { 269 - sendMIDIEvent(au, status: 0xC0, data1: program) 262 + selectMelodicProgram(au, program: program) 270 263 return 271 264 } 272 265 // Sampler fallback.