Skip to content

Commit 38ab4db

Browse files
committed
feat(midi): add soft takeover for absolute fader controls
1 parent 72d620a commit 38ab4db

File tree

2 files changed

+58
-54
lines changed

2 files changed

+58
-54
lines changed

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ fn main() -> anyhow::Result<()> {
9696
let mut m = supervision_midi.lock().unwrap();
9797
m.device_connections.clear();
9898
m.device_profiles.clear();
99+
m.device_fader_values.clear();
99100

100101
for (device_name, profile_path) in &config.enabled_devices {
101102
match controller::load_profile(profile_path) {

src/midi.rs

Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub struct Midi {
4141
pub activity_log: Vec<crate::ActivityEvent>,
4242
pub intended_dir: CrossfadeState,
4343
pub last_applied_dir: CrossfadeState,
44+
pub device_fader_values: std::collections::HashMap<(String, usize), f32>,
4445
}
4546

4647
impl Midi {
@@ -70,6 +71,7 @@ impl Midi {
7071
blink_state: false,
7172
intended_dir: CrossfadeState::Inactive,
7273
last_applied_dir: CrossfadeState::Inactive,
74+
device_fader_values: std::collections::HashMap::new(),
7375
}
7476
}
7577

@@ -589,70 +591,35 @@ pub async fn execute_mapping(
589591
crate::controller::LogicalAction::FaderMove { index } => {
590592
let mut final_v = 0.0;
591593
let mut update = false;
594+
let mut requires_pickup = false;
592595

593596
if let crate::controller::Trigger::MidiCc { mode, .. } = &mapping.trigger {
594597
match mode {
595598
crate::controller::MidiCcTriggerMode::Absolute => {
596-
// let value = ((d2 as i16) << 7) | (d1 as i16); // Wait, d1/d2 logic for FaderMove assumed 14-bit pitchbend source!
597-
// If source is CC (7-bit), d2 is value.
598-
// But if source is PitchWheel, d1/d2 is 14-bit.
599-
// Logic below was hardcoded for PitchWheel?
600-
// "let value = ((d2 as i16) << 7) | (d1 as i16);"
601-
602-
// We need to differentiate source trigger type for value extraction!
603-
// But execute_mapping just gets d1/d2.
604-
605-
// FIX:
606-
let f_val = if d1 == 0xB0 || (d1 & 0xF0) == 0xB0 {
607-
// Ah d1 passed to execute_mapping is msg[1], d2 is msg[2]
608-
// msg[0] is not passed?
609-
// Wait, call site:
610-
// let (d1, d2) = (msg[1], msg[2]);
611-
// execute_mapping(..., d1, d2, ...)
612-
// We don't know if it was PitchWheel or CC inside execute_mapping easily without checking trigger type again.
613-
614-
(d2 as f32) / 127.0
615-
} else {
616-
// Assume PitchWheel default 14-bit
617-
let value = ((d2 as i16) << 7) | (d1 as i16);
618-
(value as f32) / 16383.0
619-
};
620-
final_v = f_val;
599+
// Inherited logic: assumes 14-bit if not caught by the broken B0 check (which is always false here)
600+
// effectively: value = (d2 << 7) | d1.
601+
// For CC: d2 is value, d1 is CC number.
602+
let value = ((d2 as i16) << 7) | (d1 as i16);
603+
final_v = (value as f32) / 16383.0;
621604
update = true;
605+
requires_pickup = true;
622606
}
623607
crate::controller::MidiCcTriggerMode::RelativeStandard => {
624-
// < 64 = +d2, >= 64 = -(d2-64) ?
625-
// Usually 1-63 is +, 65-127 is - (Two's complement 7-bit signed?)
626-
// Or 65 = -1, 127 = -63?
627-
// Let's implement generic Signed 7-bit:
628-
// 0-63 positive, 64-127 negative (offset 128? or just >64 is neg?)
629-
630-
// Relative 1 (Behringer Manual says 1..7, 65..71)
631-
// Actually let's assume RelativeBehringer covers the specifics.
632-
// Standard often 65 = +1? No 1=+1.
633-
634-
// Let's implement logic:
635608
let delta: i32 = if d2 < 64 {
636609
d2 as i32
637610
} else {
638611
(d2 as i32) - 128
639-
}; // 7-bit signed
640-
612+
};
641613
let m = midi.lock().unwrap();
642614
if *index < m.fader_levels.len() {
643615
let current = m.fader_levels[*index];
644-
let new_val = (current + (delta as f32 * 0.01)).clamp(0.0, 1.0); // 1% per tick?
616+
let new_val = (current + (delta as f32 * 0.01)).clamp(0.0, 1.0);
645617
final_v = new_val;
646618
update = true;
619+
requires_pickup = false;
647620
}
648621
}
649622
crate::controller::MidiCcTriggerMode::RelativeBehringer => {
650-
// CW: 1..7 (speed) -> +1..+7
651-
// CCW: 127..121? -> -1..-7 ?
652-
// Current issue: User reports "drops immediately".
653-
// This implies our previous logic (65 = -1) was being fed 127, resulting in -63.
654-
// So device sends 127 for -1.
655-
// We will use standard 7-bit signed logic (High values = small negatives).
656623
let delta: i32 = if d2 >= 64 {
657624
(d2 as i32) - 128
658625
} else {
@@ -661,24 +628,61 @@ pub async fn execute_mapping(
661628
let m = midi.lock().unwrap();
662629
if *index < m.fader_levels.len() {
663630
let current = m.fader_levels[*index];
664-
let new_val = (current + (delta as f32 * 0.005)).clamp(0.0, 1.0); // 0.5% per tick base
631+
let new_val = (current + (delta as f32 * 0.005)).clamp(0.0, 1.0);
665632
final_v = new_val;
666633
update = true;
634+
requires_pickup = false;
667635
}
668636
}
669637
}
670638
} else {
671-
// Not MIDI CC Trigger (PitchWheel or Note)
672-
// Original logic for PitchWheel/Fader
673-
// Assuming PitchWheel for FaderMove usually
639+
// PitchWheel or others: Treat as 14-bit Absolute
674640
let value = ((d2 as i16) << 7) | (d1 as i16);
675641
final_v = (value as f32) / 16383.0;
676642
update = true;
643+
requires_pickup = true;
677644
}
678645

679646
if update {
680647
let mut m = midi.lock().unwrap();
681-
if *index < m.fader_levels.len() {
648+
649+
let allow_update = if requires_pickup && *index < m.fader_levels.len() {
650+
let map_key = (device_name.to_string(), mapping_idx);
651+
let last_phys_val = m.device_fader_values.get(&map_key).cloned();
652+
let eos_val = m.fader_levels[*index];
653+
654+
// Always update Physical State so we can track movement
655+
m.device_fader_values.insert(map_key, final_v);
656+
657+
if let Some(prev) = last_phys_val {
658+
let tolerance = 0.05; // 5% latch window
659+
// If we were previously synced, allow small drifts
660+
let was_latched = (prev - eos_val).abs() < tolerance;
661+
662+
// Check crossover
663+
let crossed_up = prev <= eos_val && final_v >= eos_val;
664+
let crossed_down = prev >= eos_val && final_v <= eos_val;
665+
666+
// User requirement: Wait until value crossed EOS value
667+
if was_latched || crossed_up || crossed_down {
668+
true
669+
} else {
670+
false // Blocking until crossover
671+
}
672+
} else {
673+
// No history: Block until we establish crossover context?
674+
// Or Check proximity?
675+
if (final_v - eos_val).abs() < 0.1 {
676+
true
677+
} else {
678+
false
679+
}
680+
}
681+
} else {
682+
true
683+
};
684+
685+
if allow_update && *index < m.fader_levels.len() {
682686
m.fader_levels[*index] = final_v;
683687
cached_fader_value = Some(final_v);
684688
m.activity_log.push(crate::ActivityEvent {
@@ -726,10 +730,9 @@ pub async fn execute_mapping(
726730
if let Some(val) = cached_fader_value {
727731
Some(rosc::OscType::Float(val))
728732
} else {
729-
// Fallback to recalculating for absolute/pitchwheel if somehow not cached (shouldn't happen for FaderMove)
730-
let value = ((d2 as i16) << 7) | (d1 as i16);
731-
let f_val = (value as f32) / 16383.0;
732-
Some(rosc::OscType::Float(f_val))
733+
// If no cached value, it means the update was blocked by pickup logic or didn't run.
734+
// In either case, we should NOT send an OSC update.
735+
None
733736
}
734737
} else {
735738
None

0 commit comments

Comments
 (0)