Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

Align piano mouse input with octave setting

authored by

Esteban Uribe and committed by
prompt.ac/@jeffrey
577ef037 b213c125

+100 -38
+40 -15
slab/menuband/Sources/MenuBand/AppDelegate.swift
··· 1189 1189 let initialHitPt = imagePoint(from: downEvent.locationInWindow) 1190 1190 let initial = KeyboardIconRenderer.hit(at: initialHitPt) 1191 1191 debugLog("hit pt=(\(initialHitPt.x),\(initialHitPt.y)) -> \(String(describing: initial))") 1192 - let startNote: UInt8 1192 + let startDisplayNote: UInt8 1193 1193 switch initial { 1194 1194 case .openSettings: 1195 1195 showPopover() ··· 1202 1202 floatingPlayPalette.show() 1203 1203 return 1204 1204 case .note(let n): 1205 - startNote = n 1205 + startDisplayNote = n 1206 1206 case .none: 1207 1207 return 1208 1208 } 1209 1209 1210 + guard let startNote = playedNote(for: startDisplayNote) else { return } 1211 + 1210 1212 let initialPt = imagePoint(from: downEvent.locationInWindow) 1211 - let (vel0, pan0) = NoteExpression.values(for: startNote, at: initialPt) 1213 + let (vel0, pan0) = NoteExpression.values(for: startDisplayNote, at: initialPt) 1212 1214 let initialShift = downEvent.modifierFlags.contains(.shift) 1213 1215 || downEvent.modifierFlags.contains(.capsLock) 1214 - menuBand.startTapNote(startNote, velocity: vel0, pan: pan0, linger: initialShift) 1216 + menuBand.startTapNote( 1217 + startNote, 1218 + velocity: vel0, 1219 + pan: pan0, 1220 + displayNote: startDisplayNote, 1221 + linger: initialShift 1222 + ) 1215 1223 waveformStrip.showIfNeeded() 1216 1224 // Arm sandbox-friendly local capture on a real piano click. We 1217 1225 // skip arming when global TYPE mode is already on — the global ··· 1223 1231 localCapture.arm() 1224 1232 floatingPlayPalette.refresh() 1225 1233 } 1226 - var current: UInt8? = startNote 1234 + var currentDisplay: UInt8? = startDisplayNote 1235 + var currentPlayed: UInt8? = startNote 1227 1236 while let next = NSApp.nextEvent( 1228 1237 matching: [.leftMouseDragged, .leftMouseUp], 1229 1238 until: .distantFuture, ··· 1231 1240 dequeue: true 1232 1241 ) { 1233 1242 if next.type == .leftMouseUp { 1234 - if let c = current { menuBand.stopTapNote(c) } 1243 + if let c = currentPlayed { menuBand.stopTapNote(c) } 1235 1244 break 1236 1245 } 1237 1246 let pt = imagePoint(from: next.locationInWindow) 1238 - let hovered = KeyboardIconRenderer.noteAt(pt) 1239 - if hovered != current { 1240 - if let prev = current { menuBand.stopTapNote(prev) } 1241 - if let nxt = hovered { 1242 - let (v, p) = NoteExpression.values(for: nxt, at: pt) 1247 + let hoveredDisplay = KeyboardIconRenderer.noteAt(pt) 1248 + if hoveredDisplay != currentDisplay { 1249 + if let prev = currentPlayed { menuBand.stopTapNote(prev) } 1250 + if let nxtDisplay = hoveredDisplay, 1251 + let nxtPlayed = playedNote(for: nxtDisplay) { 1252 + let (v, p) = NoteExpression.values(for: nxtDisplay, at: pt) 1243 1253 // Sample shift+capslock state per-note during the drag 1244 1254 // so the user can shift-press, release shift mid-drag, 1245 1255 // and still get linger from a latched caps lock. 1246 1256 let shiftNow = next.modifierFlags.contains(.shift) 1247 1257 || next.modifierFlags.contains(.capsLock) 1248 - menuBand.startTapNote(nxt, velocity: v, pan: p, linger: shiftNow) 1258 + menuBand.startTapNote( 1259 + nxtPlayed, 1260 + velocity: v, 1261 + pan: p, 1262 + displayNote: nxtDisplay, 1263 + linger: shiftNow 1264 + ) 1249 1265 waveformStrip.showIfNeeded() 1266 + currentPlayed = nxtPlayed 1267 + } else { 1268 + currentPlayed = nil 1250 1269 } 1251 - current = hovered 1252 - } else if let c = current { 1253 - let (_, p) = NoteExpression.values(for: c, at: pt) 1270 + currentDisplay = hoveredDisplay 1271 + } else if let c = currentPlayed, let display = currentDisplay { 1272 + let (_, p) = NoteExpression.values(for: display, at: pt) 1254 1273 menuBand.updateTapPan(c, pan: p) 1255 1274 } 1256 1275 } 1276 + } 1277 + 1278 + private func playedNote(for displayNote: UInt8) -> UInt8? { 1279 + let value = Int(displayNote) + menuBand.octaveShift * 12 1280 + guard value >= 0, value <= 127 else { return nil } 1281 + return UInt8(value) 1257 1282 } 1258 1283 1259 1284 // MARK: - Popover
+46 -18
slab/menuband/Sources/MenuBand/FloatingPlayPalette.swift
··· 965 965 private weak var menuBand: MenuBandController? 966 966 private var trackingArea: NSTrackingArea? 967 967 private var hoveredNote: UInt8? 968 - private var currentNote: UInt8? 968 + private var currentDisplayNote: UInt8? 969 + private var currentPlayedNote: UInt8? 969 970 970 971 private let pianoScale: CGFloat 971 972 private var widthConstraint: NSLayoutConstraint! ··· 1055 1056 window?.makeKey() 1056 1057 guard let menuBand = menuBand, 1057 1058 let point = rendererPoint(from: event), 1058 - let note = withFloatingPaletteKeyboard( 1059 + let displayNote = withFloatingPaletteKeyboard( 1059 1060 menuBand: menuBand, 1060 1061 { KeyboardIconRenderer.noteAt(point, layout: Self.rendererLayout) } 1061 - ) 1062 + ), 1063 + let playedNote = playedNote(for: displayNote, menuBand: menuBand) 1062 1064 else { return } 1063 1065 let expression = withFloatingPaletteKeyboard(menuBand: menuBand) { 1064 - NoteExpression.values(for: note, at: point, layout: Self.rendererLayout) 1066 + NoteExpression.values(for: displayNote, at: point, layout: Self.rendererLayout) 1065 1067 } 1066 - currentNote = note 1067 - hoveredNote = note 1068 - menuBand.startTapNote(note, velocity: expression.velocity, pan: expression.pan) 1068 + currentDisplayNote = displayNote 1069 + currentPlayedNote = playedNote 1070 + hoveredNote = displayNote 1071 + menuBand.startTapNote( 1072 + playedNote, 1073 + velocity: expression.velocity, 1074 + pan: expression.pan, 1075 + displayNote: displayNote 1076 + ) 1069 1077 needsDisplay = true 1070 1078 } 1071 1079 ··· 1078 1086 ) 1079 1087 hoveredNote = hovered 1080 1088 1081 - if hovered != currentNote { 1082 - if let previous = currentNote { 1089 + if hovered != currentDisplayNote { 1090 + if let previous = currentPlayedNote { 1083 1091 menuBand.stopTapNote(previous) 1084 1092 } 1085 - if let next = hovered { 1093 + if let nextDisplay = hovered, 1094 + let nextPlayed = playedNote(for: nextDisplay, menuBand: menuBand) { 1086 1095 let expression = withFloatingPaletteKeyboard(menuBand: menuBand) { 1087 - NoteExpression.values(for: next, at: point, layout: Self.rendererLayout) 1096 + NoteExpression.values(for: nextDisplay, at: point, layout: Self.rendererLayout) 1088 1097 } 1089 - menuBand.startTapNote(next, velocity: expression.velocity, pan: expression.pan) 1098 + menuBand.startTapNote( 1099 + nextPlayed, 1100 + velocity: expression.velocity, 1101 + pan: expression.pan, 1102 + displayNote: nextDisplay 1103 + ) 1104 + currentPlayedNote = nextPlayed 1105 + } else { 1106 + currentPlayedNote = nil 1090 1107 } 1091 - currentNote = hovered 1092 - } else if let current = currentNote { 1108 + currentDisplayNote = hovered 1109 + } else if let current = currentDisplayNote, 1110 + currentPlayedNote != nil { 1093 1111 let expression = withFloatingPaletteKeyboard(menuBand: menuBand) { 1094 1112 NoteExpression.values(for: current, at: point, layout: Self.rendererLayout) 1095 1113 } 1096 - menuBand.updateTapPan(current, pan: expression.pan) 1114 + if let playedNote = currentPlayedNote { 1115 + menuBand.updateTapPan(playedNote, pan: expression.pan) 1116 + } 1097 1117 } 1098 1118 needsDisplay = true 1099 1119 } 1100 1120 1101 1121 override func mouseUp(with event: NSEvent) { 1102 - if let note = currentNote { 1122 + if let note = currentPlayedNote { 1103 1123 menuBand?.stopTapNote(note) 1104 1124 } 1105 - currentNote = nil 1125 + currentDisplayNote = nil 1126 + currentPlayedNote = nil 1106 1127 updateHover(with: event) 1107 1128 } 1108 1129 1109 1130 func clearInteraction() { 1110 - currentNote = nil 1131 + currentDisplayNote = nil 1132 + currentPlayedNote = nil 1111 1133 hoveredNote = nil 1112 1134 needsDisplay = true 1113 1135 } ··· 1158 1180 width: size.width, 1159 1181 height: size.height 1160 1182 ) 1183 + } 1184 + 1185 + private func playedNote(for displayNote: UInt8, menuBand: MenuBandController) -> UInt8? { 1186 + let value = Int(displayNote) + menuBand.octaveShift * 12 1187 + guard value >= 0, value <= 127 else { return nil } 1188 + return UInt8(value) 1161 1189 } 1162 1190 } 1163 1191
+14 -5
slab/menuband/Sources/MenuBand/MenuBandController.swift
··· 678 678 679 679 private var tapHeld: Set<UInt8> = [] // notes currently held by mouse drag (main thread) 680 680 private var tapNoteChannel: [UInt8: UInt8] = [:] // active channel per held note 681 + private var tapDisplayNote: [UInt8: UInt8] = [:] // visible key to light for a held tap note 681 682 private var tapLinger: [UInt8: UInt8] = [:] // notes started under shift → noteOn velocity (drives doppler) 682 683 // Live notes round-robin across channels 0-3; doppler retriggers 683 684 // use their own 4-7 cursor. The split guarantees a live press ··· 727 728 /// handler computes them from the cursor's relative position inside the 728 729 /// hovered key, giving expressive control: y closer to vertical center = 729 730 /// louder, x within key = stereo pan. 730 - func startTapNote(_ midiNote: UInt8, velocity: UInt8 = 100, pan: UInt8 = 64, linger: Bool = false) { 731 + func startTapNote(_ midiNote: UInt8, 732 + velocity: UInt8 = 100, 733 + pan: UInt8 = 64, 734 + displayNote: UInt8? = nil, 735 + linger: Bool = false) { 731 736 debugLog("startTapNote midi=\(midiNote) midiMode=\(midiMode) linger=\(linger)") 732 737 lastPlayedNote = midiNote 733 738 if tapHeld.contains(midiNote) { return } 734 739 tapHeld.insert(midiNote) 740 + let visualNote = displayNote ?? midiNote 741 + tapDisplayNote[midiNote] = visualNote 735 742 if linger { tapLinger[midiNote] = velocity } 736 743 let isDrum = midiNote < UInt8(KeyboardIconRenderer.firstMidi) 737 744 // Synth: rotate across 8 channels so rapid same-note taps overlap ··· 750 757 // blink wasn't visible. 751 758 let setLit = { [weak self] in 752 759 guard let self = self else { return } 753 - self.litDownAt[midiNote] = CACurrentMediaTime() 754 - if self.litNotes.insert(midiNote).inserted { 760 + self.litDownAt[visualNote] = CACurrentMediaTime() 761 + if self.litNotes.insert(visualNote).inserted { 755 762 self.onLitChanged?() 756 763 } 757 764 } ··· 770 777 func stopTapNote(_ midiNote: UInt8) { 771 778 guard tapHeld.contains(midiNote) else { return } 772 779 tapHeld.remove(midiNote) 780 + let visualNote = tapDisplayNote.removeValue(forKey: midiNote) ?? midiNote 773 781 let lingerVelocity = tapLinger.removeValue(forKey: midiNote) 774 782 let synthCh = tapNoteChannel.removeValue(forKey: midiNote) ?? channel(for: midiNote) 775 783 let isDrum = midiNote < UInt8(KeyboardIconRenderer.firstMidi) ··· 794 802 // matches the keyboard path now. 795 803 DispatchQueue.main.async { [weak self] in 796 804 guard let self = self else { return } 797 - self.litDownAt.removeValue(forKey: midiNote) 798 - if self.litNotes.remove(midiNote) != nil { 805 + self.litDownAt.removeValue(forKey: visualNote) 806 + if self.litNotes.remove(visualNote) != nil { 799 807 self.onLitChanged?() 800 808 } 801 809 } ··· 1189 1197 let tapChanSnapshot = tapNoteChannel 1190 1198 tapHeld.removeAll() 1191 1199 tapNoteChannel.removeAll() 1200 + tapDisplayNote.removeAll() 1192 1201 for (keyCode, note) in noteSnapshot { 1193 1202 let ch = chanSnapshot[keyCode] ?? 0 1194 1203 synth.noteOff(note, channel: ch)