Add velocity support to virtual piano
This commit is contained in:
parent
98c2880b45
commit
c09cd276a0
|
|
@ -1178,9 +1178,13 @@ impl eframe::App for EditorApp {
|
|||
let wants_keyboard = ctx.wants_keyboard_input();
|
||||
|
||||
ctx.input(|i| {
|
||||
// Check menu shortcuts (these use modifiers, so allow even when typing)
|
||||
// Check menu shortcuts that use modifiers (Cmd+S, etc.) - allow even when typing
|
||||
// But skip shortcuts without modifiers when keyboard input is claimed (e.g., virtual piano)
|
||||
if let Some(action) = MenuSystem::check_shortcuts(i) {
|
||||
self.handle_menu_action(action);
|
||||
// Only trigger if keyboard isn't claimed OR the shortcut uses modifiers
|
||||
if !wants_keyboard || i.modifiers.ctrl || i.modifiers.command || i.modifiers.alt || i.modifiers.shift {
|
||||
self.handle_menu_action(action);
|
||||
}
|
||||
}
|
||||
|
||||
// Check tool shortcuts (only if no modifiers are held AND no text input is focused)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ pub struct VirtualPianoPane {
|
|||
keyboard_map: HashMap<String, u8>,
|
||||
/// Reverse mapping for displaying labels (MIDI note -> key label)
|
||||
note_to_key_map: HashMap<u8, String>,
|
||||
/// Sustain pedal state (Tab key toggles)
|
||||
sustain_active: bool,
|
||||
/// Notes being held by sustain pedal (not by active key/mouse press)
|
||||
sustained_notes: HashSet<u8>,
|
||||
}
|
||||
|
||||
impl Default for VirtualPianoPane {
|
||||
|
|
@ -77,6 +81,8 @@ impl VirtualPianoPane {
|
|||
keyboard_velocity: 100, // Default MIDI velocity
|
||||
keyboard_map,
|
||||
note_to_key_map,
|
||||
sustain_active: false,
|
||||
sustained_notes: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -235,7 +241,11 @@ impl VirtualPianoPane {
|
|||
if !black_key_interacted {
|
||||
// Mouse down starts note (detect primary button pressed on this key)
|
||||
if pointer_over_key && ui.input(|i| i.pointer.primary_pressed()) {
|
||||
self.send_note_on(note, 100, shared);
|
||||
// Calculate velocity based on mouse Y position
|
||||
let mouse_y = ui.input(|i| i.pointer.hover_pos()).unwrap().y;
|
||||
let velocity = self.calculate_velocity_from_mouse_y(mouse_y, key_rect);
|
||||
|
||||
self.send_note_on(note, velocity, shared);
|
||||
self.dragging_note = Some(note);
|
||||
}
|
||||
|
||||
|
|
@ -254,8 +264,11 @@ impl VirtualPianoPane {
|
|||
if let Some(prev_note) = self.dragging_note {
|
||||
self.send_note_off(prev_note, shared);
|
||||
}
|
||||
// Start new note
|
||||
self.send_note_on(note, 100, shared);
|
||||
// Start new note with velocity from mouse position
|
||||
let mouse_y = ui.input(|i| i.pointer.hover_pos()).unwrap().y;
|
||||
let velocity = self.calculate_velocity_from_mouse_y(mouse_y, key_rect);
|
||||
|
||||
self.send_note_on(note, velocity, shared);
|
||||
self.dragging_note = Some(note);
|
||||
}
|
||||
}
|
||||
|
|
@ -304,7 +317,11 @@ impl VirtualPianoPane {
|
|||
|
||||
// Mouse down starts note
|
||||
if pointer_over_key && ui.input(|i| i.pointer.primary_pressed()) {
|
||||
self.send_note_on(note, 100, shared);
|
||||
// Calculate velocity based on mouse Y position
|
||||
let mouse_y = ui.input(|i| i.pointer.hover_pos()).unwrap().y;
|
||||
let velocity = self.calculate_velocity_from_mouse_y(mouse_y, key_rect);
|
||||
|
||||
self.send_note_on(note, velocity, shared);
|
||||
self.dragging_note = Some(note);
|
||||
}
|
||||
|
||||
|
|
@ -322,7 +339,11 @@ impl VirtualPianoPane {
|
|||
if let Some(prev_note) = self.dragging_note {
|
||||
self.send_note_off(prev_note, shared);
|
||||
}
|
||||
self.send_note_on(note, 100, shared);
|
||||
// Start new note with velocity from mouse position
|
||||
let mouse_y = ui.input(|i| i.pointer.hover_pos()).unwrap().y;
|
||||
let velocity = self.calculate_velocity_from_mouse_y(mouse_y, key_rect);
|
||||
|
||||
self.send_note_on(note, velocity, shared);
|
||||
self.dragging_note = Some(note);
|
||||
}
|
||||
}
|
||||
|
|
@ -344,9 +365,18 @@ impl VirtualPianoPane {
|
|||
}
|
||||
}
|
||||
|
||||
/// Send note-off event to daw-backend
|
||||
/// Send note-off event to daw-backend (or add to sustain if active)
|
||||
fn send_note_off(&mut self, note: u8, shared: &mut SharedPaneState) {
|
||||
// If sustain is active, move note to sustained set instead of releasing
|
||||
if self.sustain_active {
|
||||
self.sustained_notes.insert(note);
|
||||
// Keep note in pressed_notes for visual feedback
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal release: remove from all note sets
|
||||
self.pressed_notes.remove(¬e);
|
||||
self.sustained_notes.remove(¬e);
|
||||
|
||||
if let Some(active_layer_id) = *shared.active_layer_id {
|
||||
if let Some(&track_id) = shared.layer_to_track_map.get(&active_layer_id) {
|
||||
|
|
@ -357,6 +387,65 @@ impl VirtualPianoPane {
|
|||
}
|
||||
}
|
||||
|
||||
/// Release sustain pedal - stop all sustained notes that aren't currently being held
|
||||
fn release_sustain(&mut self, shared: &mut SharedPaneState) {
|
||||
self.sustain_active = false;
|
||||
|
||||
// Collect currently active notes (keyboard + mouse)
|
||||
let mut currently_playing = HashSet::new();
|
||||
|
||||
// Add notes from keyboard
|
||||
for ¬e in self.active_key_presses.values() {
|
||||
currently_playing.insert(note);
|
||||
}
|
||||
|
||||
// Add note from mouse drag
|
||||
if let Some(note) = self.dragging_note {
|
||||
currently_playing.insert(note);
|
||||
}
|
||||
|
||||
// Release sustained notes that aren't currently being played
|
||||
let notes_to_release: Vec<u8> = self.sustained_notes
|
||||
.iter()
|
||||
.filter(|&¬e| !currently_playing.contains(¬e))
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
for note in notes_to_release {
|
||||
self.send_note_off(note, shared);
|
||||
}
|
||||
|
||||
self.sustained_notes.clear();
|
||||
}
|
||||
|
||||
/// Calculate MIDI velocity based on mouse Y position within key
|
||||
///
|
||||
/// - Top of key: velocity 1
|
||||
/// - Top 75% of key: Linear scaling from velocity 1 to 127
|
||||
/// - Bottom 25% of key: Full velocity (127)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `mouse_y` - Y coordinate of mouse cursor
|
||||
/// * `key_rect` - Rectangle bounds of the key
|
||||
///
|
||||
/// # Returns
|
||||
/// MIDI velocity value clamped to range [1, 127]
|
||||
fn calculate_velocity_from_mouse_y(&self, mouse_y: f32, key_rect: egui::Rect) -> u8 {
|
||||
// Calculate relative position (0.0 at top, 1.0 at bottom)
|
||||
let key_height = key_rect.height();
|
||||
let relative_y = (mouse_y - key_rect.min.y) / key_height;
|
||||
let relative_y = relative_y.clamp(0.0, 1.0);
|
||||
|
||||
// Bottom 25% of key = full velocity
|
||||
if relative_y >= 0.75 {
|
||||
return 127;
|
||||
}
|
||||
|
||||
// Top 75% = linear scale from 1 to 127
|
||||
let velocity = 1.0 + (relative_y / 0.75) * 126.0;
|
||||
velocity.round().clamp(1.0, 127.0) as u8
|
||||
}
|
||||
|
||||
/// Process keyboard input for virtual piano
|
||||
/// Returns true if the event was consumed
|
||||
fn handle_keyboard_input(&mut self, ui: &mut egui::Ui, shared: &mut SharedPaneState) -> bool {
|
||||
|
|
@ -398,6 +487,16 @@ impl VirtualPianoPane {
|
|||
consumed = true;
|
||||
}
|
||||
|
||||
// Handle sustain pedal (Tab key)
|
||||
if i.key_pressed(egui::Key::Tab) {
|
||||
self.sustain_active = true;
|
||||
consumed = true;
|
||||
}
|
||||
if i.key_released(egui::Key::Tab) {
|
||||
self.release_sustain(shared);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
// Process raw events for piano keys (need to track press/release separately)
|
||||
for event in &i.events {
|
||||
if let egui::Event::Key { key, pressed, repeat, .. } = event {
|
||||
|
|
@ -457,9 +556,31 @@ impl VirtualPianoPane {
|
|||
fn release_all_keyboard_notes(&mut self, shared: &mut SharedPaneState) {
|
||||
let notes_to_release: Vec<u8> = self.active_key_presses.values().copied().collect();
|
||||
for note in notes_to_release {
|
||||
self.send_note_off(note, shared);
|
||||
// Force release, bypassing sustain
|
||||
self.pressed_notes.remove(¬e);
|
||||
if let Some(active_layer_id) = *shared.active_layer_id {
|
||||
if let Some(&track_id) = shared.layer_to_track_map.get(&active_layer_id) {
|
||||
if let Some(ref mut controller) = shared.audio_controller {
|
||||
controller.send_midi_note_off(track_id, note);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.active_key_presses.clear();
|
||||
|
||||
// Also release all sustained notes
|
||||
for note in &self.sustained_notes {
|
||||
self.pressed_notes.remove(note);
|
||||
if let Some(active_layer_id) = *shared.active_layer_id {
|
||||
if let Some(&track_id) = shared.layer_to_track_map.get(&active_layer_id) {
|
||||
if let Some(ref mut controller) = shared.audio_controller {
|
||||
controller.send_midi_note_off(track_id, *note);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.sustained_notes.clear();
|
||||
self.sustain_active = false;
|
||||
}
|
||||
|
||||
/// Render keyboard letter labels on piano keys
|
||||
|
|
@ -586,6 +707,21 @@ impl PaneRenderer for VirtualPianoPane {
|
|||
if ui.button("+").clicked() {
|
||||
self.keyboard_velocity = self.keyboard_velocity.saturating_add(10).min(127);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
// Sustain pedal indicator
|
||||
ui.label("Sustain:");
|
||||
let sustain_text = if self.sustain_active {
|
||||
egui::RichText::new("ON").color(egui::Color32::from_rgb(100, 200, 100))
|
||||
} else {
|
||||
egui::RichText::new("OFF").color(egui::Color32::GRAY)
|
||||
};
|
||||
ui.label(sustain_text);
|
||||
|
||||
if !self.sustained_notes.is_empty() {
|
||||
ui.label(format!("({} notes)", self.sustained_notes.len()));
|
||||
}
|
||||
});
|
||||
|
||||
true // We rendered a header
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"tags": ["bass", "sub", "synth"]
|
||||
},
|
||||
"midi_targets": [0],
|
||||
"output_node": 6,
|
||||
"output_node": 7,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 0,
|
||||
|
|
@ -57,6 +57,15 @@
|
|||
},
|
||||
{
|
||||
"id": 5,
|
||||
"node_type": "Gain",
|
||||
"name": "Velocity",
|
||||
"parameters": {
|
||||
"0": 1.0
|
||||
},
|
||||
"position": [1150.0, 100.0]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"node_type": "Filter",
|
||||
"name": "LP Filter",
|
||||
"parameters": {
|
||||
|
|
@ -67,7 +76,7 @@
|
|||
"position": [1300.0, 100.0]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"id": 7,
|
||||
"node_type": "AudioOutput",
|
||||
"name": "Out",
|
||||
"parameters": {},
|
||||
|
|
@ -78,9 +87,11 @@
|
|||
{ "from_node": 0, "from_port": 0, "to_node": 1, "to_port": 0 },
|
||||
{ "from_node": 1, "from_port": 0, "to_node": 2, "to_port": 0 },
|
||||
{ "from_node": 1, "from_port": 1, "to_node": 3, "to_port": 0 },
|
||||
{ "from_node": 1, "from_port": 2, "to_node": 5, "to_port": 1 },
|
||||
{ "from_node": 2, "from_port": 0, "to_node": 4, "to_port": 0 },
|
||||
{ "from_node": 3, "from_port": 0, "to_node": 4, "to_port": 1 },
|
||||
{ "from_node": 4, "from_port": 0, "to_node": 5, "to_port": 0 },
|
||||
{ "from_node": 5, "from_port": 0, "to_node": 6, "to_port": 0 }
|
||||
{ "from_node": 5, "from_port": 0, "to_node": 6, "to_port": 0 },
|
||||
{ "from_node": 6, "from_port": 0, "to_node": 7, "to_port": 0 }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue