From 715c38006b3329adbe67490db6426104bec6cc68 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 09:54:22 +0000 Subject: [PATCH 1/9] feat(midi): Add music-reactive LED feedback with intensity Allow MIDI mixer LEDs to pulse from live audio peaks instead of only mirroring fader/button state. Users can set Music Reactive mode, adjust intensity, and keep using the existing custom Note/CC output assignment. Co-authored-by: Pezbino --- docs/USER_GUIDE.md | 11 ++ src-tauri/src/audio/mod.rs | 21 +++ src-tauri/src/audio/windows.rs | 146 ++++++++++++++- src-tauri/src/background_tasks.rs | 106 +++++++++-- .../src/binding_services/feedback_outputs.rs | 18 ++ .../src/binding_services/feedback_updates.rs | 19 ++ src-tauri/src/led_feedback.rs | 174 ++++++++++++++++++ src-tauri/src/lib.rs | 1 + src-tauri/src/model.rs | 6 +- src-tauri/src/model/binding_types.rs | 21 ++- src-tauri/src/model/control_types.rs | 17 ++ src-tauri/src/model/tests.rs | 19 ++ src-tauri/src/test_support.rs | 2 + src/app/dom_refs.js | 8 + src/core/binding_model.js | 3 + src/features/bindings/bindings.js | 2 + .../bindings/controllers/config_events.js | 40 ++++ .../bindings/controllers/feedback_editor.js | 65 +++++++ src/features/bindings/template.js | 33 +++- src/locales/de.json | 8 +- src/locales/en.json | 8 +- src/locales/es.json | 8 +- src/locales/fr.json | 8 +- src/locales/it.json | 8 +- src/locales/ja.json | 8 +- src/locales/ko.json | 8 +- src/locales/nl.json | 8 +- src/locales/pl.json | 8 +- src/locales/pt-BR.json | 8 +- src/locales/zh-Hans.json | 8 +- .../config-panel/feedback-controls.css | 24 +++ 31 files changed, 791 insertions(+), 33 deletions(-) create mode 100644 src-tauri/src/led_feedback.rs diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index f70a680..f9c15aa 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -17,6 +17,17 @@ MIDIMaster binds controls on your MIDI device (faders, knobs, buttons) to target When a target changes, MIDIMaster can send feedback back to your MIDI controller (for motorized faders, LEDs, etc.). +### Music-reactive LEDs + +For fader and button bindings, you can switch LED feedback to **Music Reactive** mode: + +1. Open the binding configuration. +2. For faders/knobs, set **LED mode** to **Music Reactive**. For buttons, choose **Music Reactive** in the button light list. +3. Adjust **LED intensity** so quiet audio stays dim and louder peaks flash brighter. +4. Optionally learn or type a custom Note/CC/Pitch Bend address for the LED output (already supported under Feedback output / Indicator output). + +In Music Reactive mode the light follows live audio peaks for the bound target (master, device, application, session, or focused app). Quiet passages stay dark; louder beats drive brighter pulses. If a fader also has a custom LED address, motor fader position feedback still updates the primary control while the LED address receives the reactive signal. + ## First-Time Setup ### 1) Configure MIDI Routes diff --git a/src-tauri/src/audio/mod.rs b/src-tauri/src/audio/mod.rs index 46435fc..c7a7c19 100644 --- a/src-tauri/src/audio/mod.rs +++ b/src-tauri/src/audio/mod.rs @@ -1,4 +1,22 @@ use crate::model::SessionInfo; +use std::collections::HashMap; + +#[derive(Debug, Clone, Default)] +pub struct SessionPeakIdentity { + pub display_name: String, + pub application_key: Option, + pub process_name: Option, + pub process_path: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct AudioPeakLevels { + pub master: f32, + pub devices: HashMap, + pub sessions: HashMap, + pub focused_session_id: Option, + pub session_matches: Vec<(SessionPeakIdentity, f32)>, +} pub trait AudioBackend: Send + Sync { fn list_sessions(&self) -> anyhow::Result>; @@ -16,6 +34,9 @@ pub trait AudioBackend: Send + Sync { fn focused_session_state(&self) -> anyhow::Result> { self.focused_session() } + fn peak_levels(&self) -> anyhow::Result { + Ok(AudioPeakLevels::default()) + } // Mute methods fn set_master_mute(&self, muted: bool) -> anyhow::Result<()>; diff --git a/src-tauri/src/audio/windows.rs b/src-tauri/src/audio/windows.rs index 02153c3..c5601b5 100644 --- a/src-tauri/src/audio/windows.rs +++ b/src-tauri/src/audio/windows.rs @@ -1,5 +1,5 @@ use crate::audio::target_match::{application_name_matches, ApplicationMatchInfo}; -use crate::audio::AudioBackend; +use crate::audio::{AudioBackend, AudioPeakLevels, SessionPeakIdentity}; use crate::device_target::{parse_device_target, DeviceTargetKind}; use crate::model::{PlaybackDeviceInfo, SessionInfo}; use anyhow::{anyhow, Result}; @@ -11,7 +11,7 @@ use std::sync::{Mutex, OnceLock}; use std::time::{Duration, Instant}; use windows::core::{IUnknown_Vtbl, Interface, GUID, HRESULT, PCWSTR}; use windows::Win32::Foundation::{PROPERTYKEY, RPC_E_CHANGED_MODE}; -use windows::Win32::Media::Audio::Endpoints::IAudioEndpointVolume; +use windows::Win32::Media::Audio::Endpoints::{IAudioEndpointVolume, IAudioMeterInformation}; use windows::Win32::Media::Audio::{ eCapture, eCommunications, eConsole, eMultimedia, eRender, EDataFlow, ERole, IAudioSessionControl2, IAudioSessionManager2, IMMDevice, IMMDeviceEnumerator, @@ -208,6 +208,10 @@ impl AudioBackend for WindowsAudioBackend { list_sessions_with_visuals(false) } + fn peak_levels(&self) -> Result { + collect_peak_levels() + } + fn list_playback_devices(&self) -> Result> { let _com = init_com()?; let enumerator = get_device_enumerator()?; @@ -1078,6 +1082,144 @@ fn get_endpoint_volume( Ok(endpoint) } +fn endpoint_peak_value(device: &IMMDevice) -> f32 { + let Ok(meter) = (|| -> Result { + Ok(unsafe { device.Activate(CLSCTX_ALL, None)? }) + })() else { + return 0.0; + }; + unsafe { meter.GetPeakValue() } + .ok() + .map(|value| value.clamp(0.0, 1.0)) + .unwrap_or(0.0) +} + +fn session_peak_value(control: &IAudioSessionControl2, simple: &ISimpleAudioVolume) -> f32 { + if unsafe { simple.GetMute() } + .ok() + .map(|muted| muted.as_bool()) + .unwrap_or(false) + { + return 0.0; + } + let Ok(meter) = control.cast::() else { + return 0.0; + }; + unsafe { meter.GetPeakValue() } + .ok() + .map(|value| value.clamp(0.0, 1.0)) + .unwrap_or(0.0) +} + +fn collect_peak_levels() -> Result { + let _com = init_com()?; + let enumerator = get_device_enumerator()?; + let default_device = get_default_device_from(&enumerator)?; + let default_device_id = device_id_string(&default_device); + let master = endpoint_peak_value(&default_device); + + let mut devices = HashMap::new(); + for (device, device_id) in enumerate_active_devices(&enumerator, eRender)? { + devices.insert(device_id, endpoint_peak_value(&device)); + } + + let mut sessions = HashMap::new(); + let mut session_matches = Vec::new(); + let mut seen_ids = HashSet::new(); + + for (device, device_id) in enumerate_active_devices(&enumerator, eRender)? { + let _ = visit_audio_sessions::<()>(&device, |control2, simple, process_id| { + let base_id = session_identifier(control2, process_id) + .unwrap_or_else(|| format!("pid:{}", process_id)); + let session_id = if default_device_id.as_deref() == Some(device_id.as_str()) { + base_id + } else { + format!("{}|{}", device_id, base_id) + }; + if !seen_ids.insert(session_id.clone()) { + return Ok(SessionVisit::Continue); + } + + let peak = session_peak_value(control2, simple); + let identity = query_effective_process_identity_cached(process_id); + let process_path = identity.path.clone(); + let process_name = process_path + .as_ref() + .and_then(|path| Path::new(path).file_name()) + .and_then(|name| name.to_str()) + .map(|name| name.to_string()); + let raw_display_name = unsafe { control2.GetDisplayName() } + .ok() + .and_then(owned_pwstr_to_string); + let display_name = session_display_name(raw_display_name.as_deref()); + let application_key = stable_application_key( + &identity, + process_path.as_deref(), + process_name.as_deref(), + display_name.as_deref(), + ); + let friendly_name = friendly_session_name( + display_name.as_deref(), + process_path.as_deref(), + process_name.as_deref(), + &identity, + ); + + if should_skip_session( + process_id, + &display_name, + &process_name, + &process_path, + &friendly_name, + &application_key, + &identity, + ) { + return Ok(SessionVisit::Continue); + } + + sessions.insert(session_id, peak); + session_matches.push(( + SessionPeakIdentity { + display_name: friendly_name, + application_key, + process_name, + process_path, + }, + peak, + )); + Ok(SessionVisit::Continue) + }); + } + + let focused_session_id = focused_session_peak_id(&default_device, default_device_id.as_deref()); + + Ok(AudioPeakLevels { + master, + devices, + sessions, + focused_session_id, + session_matches, + }) +} + +fn focused_session_peak_id(default_device: &IMMDevice, default_device_id: Option<&str>) -> Option { + let process_id = foreground_process_id()?; + let identity = query_effective_process_identity_cached(process_id); + let device_id = default_device_id?; + session_info_for_process( + default_device, + device_id, + Some(device_id), + process_id, + &identity, + &mut HashMap::new(), + false, + ) + .ok() + .flatten() + .map(|session| session.id) +} + fn get_session_manager( device: &windows::Win32::Media::Audio::IMMDevice, ) -> Result { diff --git a/src-tauri/src/background_tasks.rs b/src-tauri/src/background_tasks.rs index ac92a70..b5feb19 100644 --- a/src-tauri/src/background_tasks.rs +++ b/src-tauri/src/background_tasks.rs @@ -1,5 +1,5 @@ use crate::bindings::BindingKey; -use crate::feedback; +use crate::feedback::{self, FeedbackControlKey}; use crate::midi_event_queue::log_queue_stats; use crate::model::{BindingTarget, Profile, SessionInfo}; use crate::run_logger; @@ -16,6 +16,7 @@ use tokio::time::sleep; const MIDI_QUEUE_BATCH_DELAY: Duration = Duration::from_millis(4); const FEEDBACK_SYNC_INTERVAL: Duration = Duration::from_millis(750); const FOCUS_FEEDBACK_SYNC_INTERVAL: Duration = Duration::from_millis(250); +const AUDIO_REACTIVE_LED_INTERVAL: Duration = Duration::from_millis(40); const FULL_FEEDBACK_RESEND_INTERVAL: Duration = Duration::from_secs(10); const LEARN_COMMIT_CHECK_INTERVAL: Duration = Duration::from_millis(25); const OSD_HIDE_CHECK_INTERVAL: Duration = Duration::from_millis(100); @@ -76,6 +77,13 @@ fn profile_has_focus_target(profile: &crate::model::Profile) -> bool { }) } +fn profile_has_audio_reactive_led(profile: &crate::model::Profile) -> bool { + profile + .bindings + .iter() + .any(|binding| binding.uses_audio_reactive_led()) +} + fn feedback_context_signature(profile: &Profile, active_routes: Vec<(String, String)>) -> String { let binding_ids = profile .bindings @@ -253,6 +261,10 @@ pub(crate) fn spawn_feedback_refresh_loop( .unwrap_or_else(Instant::now); let mut last_feedback_context = String::new(); let mut last_sent_feedback: HashMap = HashMap::new(); + let mut last_reactive_led_sync = Instant::now() + .checked_sub(AUDIO_REACTIVE_LED_INTERVAL) + .unwrap_or_else(Instant::now); + let mut reactive_led_levels: HashMap = HashMap::new(); loop { if shutdown_requested(&shutdown) { @@ -303,6 +315,10 @@ pub(crate) fn spawn_feedback_refresh_loop( .ok() .and_then(|profile| profile.clone()); let profile_active = profile.is_some(); + let has_audio_reactive_led = profile + .as_ref() + .map(profile_has_audio_reactive_led) + .unwrap_or(false); if let Some(profile) = profile.as_ref() { let sync_interval = if profile_has_focus_target(profile) { FOCUS_FEEDBACK_SYNC_INTERVAL @@ -323,6 +339,7 @@ pub(crate) fn spawn_feedback_refresh_loop( if context_changed { last_feedback_context = context; last_sent_feedback.clear(); + reactive_led_levels.clear(); } let force_feedback_resend = context_changed @@ -352,20 +369,43 @@ pub(crate) fn spawn_feedback_refresh_loop( let key = BindingKey::from_binding(binding); if binding.feedback_enabled { if let Some(volume) = feedback_snapshot.get(&key).cloned() { - let output_key = - feedback::binding_feedback_control_key(binding) - .to_binding_key(); - if should_send_feedback( - &mut last_sent_feedback, - output_key, - volume, - force_feedback_resend, - ) { - if binding.is_button_binding() { - let _ = - midi.send_binding_light_feedback(binding, volume); - } else { - let _ = midi.send_binding_feedback(binding, volume); + if binding.uses_audio_reactive_led() { + if !binding.is_button_binding() + && binding.custom_feedback_output_control().is_some() + { + let primary_key = FeedbackControlKey::from_binding(binding) + .to_binding_key(); + if should_send_feedback( + &mut last_sent_feedback, + primary_key, + volume, + force_feedback_resend, + ) { + let _ = midi.send_feedback( + &binding.device_id, + binding.control.channel, + binding.control.controller, + volume, + binding.control.msg_type.clone(), + ); + } + } + } else { + let output_key = + feedback::binding_feedback_control_key(binding) + .to_binding_key(); + if should_send_feedback( + &mut last_sent_feedback, + output_key, + volume, + force_feedback_resend, + ) { + if binding.is_button_binding() { + let _ = midi + .send_binding_light_feedback(binding, volume); + } else { + let _ = midi.send_binding_feedback(binding, volume); + } } } } @@ -392,10 +432,44 @@ pub(crate) fn spawn_feedback_refresh_loop( } } } + + if has_audio_reactive_led + && last_reactive_led_sync.elapsed() >= AUDIO_REACTIVE_LED_INTERVAL + { + last_reactive_led_sync = loop_started; + let peaks = crate::led_feedback::collect_audio_peaks(state.audio.as_ref()); + if let Ok(mut midi) = state.midi.lock() { + for binding in &profile.bindings { + let output_control = + feedback::binding_feedback_control_key(binding); + let output_key = output_control.to_binding_key(); + let previous = reactive_led_levels.get(&output_key).copied().unwrap_or(0.0); + let Some(level) = crate::led_feedback::reactive_led_level_for_binding( + binding, &peaks, previous, + ) else { + continue; + }; + reactive_led_levels.insert(output_key.clone(), level); + if should_send_feedback( + &mut last_sent_feedback, + output_key, + level, + false, + ) { + if binding.is_button_binding() { + let _ = midi.send_binding_light_feedback(binding, level); + } else { + let _ = midi.send_binding_feedback(binding, level); + } + } + } + } + } } else { last_focused_session = None; last_feedback_context.clear(); last_sent_feedback.clear(); + reactive_led_levels.clear(); } if last_osd_hide_check.elapsed() >= OSD_HIDE_CHECK_INTERVAL { @@ -453,6 +527,8 @@ pub(crate) fn spawn_feedback_refresh_loop( let next_sleep = if learn_active { LEARN_COMMIT_CHECK_INTERVAL + } else if has_audio_reactive_led { + AUDIO_REACTIVE_LED_INTERVAL } else if osd_active { OSD_HIDE_CHECK_INTERVAL } else if active_profile_has_focus_target { diff --git a/src-tauri/src/binding_services/feedback_outputs.rs b/src-tauri/src/binding_services/feedback_outputs.rs index 53ddbea..b33e186 100644 --- a/src-tauri/src/binding_services/feedback_outputs.rs +++ b/src-tauri/src/binding_services/feedback_outputs.rs @@ -11,6 +11,24 @@ pub(super) fn send_resolved_binding_feedback( if !binding.feedback_enabled { return; } + if binding.uses_audio_reactive_led() { + if binding.is_button_binding() { + return; + } + if binding.custom_feedback_output_control().is_some() { + feedback::send_feedback_to_control( + state, + &FeedbackControlKey::from_binding(binding), + FeedbackSendOptions { + value, + silent, + force_hardware_feedback, + context, + }, + ); + } + return; + } if binding.is_button_binding() { feedback::send_button_light_feedback_to_binding( state, diff --git a/src-tauri/src/binding_services/feedback_updates.rs b/src-tauri/src/binding_services/feedback_updates.rs index 402e968..9933237 100644 --- a/src-tauri/src/binding_services/feedback_updates.rs +++ b/src-tauri/src/binding_services/feedback_updates.rs @@ -52,6 +52,9 @@ pub fn update_midi_feedback( } if binding.is_button_binding() { + if binding.uses_audio_reactive_led() { + continue; + } feedback::send_button_light_feedback_to_binding( state, binding, @@ -70,6 +73,22 @@ pub fn update_midi_feedback( continue; } + if binding.uses_audio_reactive_led() { + if binding.custom_feedback_output_control().is_some() { + feedback::send_feedback_to_control( + state, + &FeedbackControlKey::from_binding(binding), + FeedbackSendOptions { + value: feedback_value, + silent: false, + force_hardware_feedback: false, + context: &format!("target_feedback_primary:{}", binding.id), + }, + ); + } + continue; + } + feedback::send_feedback_to_binding( state, binding, diff --git a/src-tauri/src/led_feedback.rs b/src-tauri/src/led_feedback.rs new file mode 100644 index 0000000..85e8ffb --- /dev/null +++ b/src-tauri/src/led_feedback.rs @@ -0,0 +1,174 @@ +use crate::audio::target_match::{application_name_matches, ApplicationMatchInfo}; +use crate::audio::{AudioPeakLevels, AudioBackend}; +use crate::device_target::{parse_device_target, DeviceTargetKind}; +use crate::model::{Binding, BindingTarget}; + +pub const LED_ENVELOPE_DECAY: f32 = 0.82; + +pub fn scale_led_level(peak: f32, intensity: f32) -> f32 { + let peak = if peak.is_finite() { + peak.clamp(0.0, 1.0) + } else { + 0.0 + }; + let intensity = if intensity.is_finite() { + intensity.clamp(0.0, 2.0) + } else { + 1.0 + }; + (peak * intensity).clamp(0.0, 1.0) +} + +pub fn envelope_led_level(previous: f32, peak: f32, intensity: f32, decay: f32) -> f32 { + let next = scale_led_level(peak, intensity); + let previous = if previous.is_finite() { + previous.clamp(0.0, 1.0) + } else { + 0.0 + }; + let decay = if decay.is_finite() { + decay.clamp(0.0, 1.0) + } else { + LED_ENVELOPE_DECAY + }; + next.max(previous * decay) +} + +pub fn peak_for_target(target: &BindingTarget, peaks: &AudioPeakLevels) -> f32 { + match target { + BindingTarget::Master => peaks.master, + BindingTarget::Focus => peaks + .focused_session_id + .as_ref() + .and_then(|id| peaks.sessions.get(id).copied()) + .unwrap_or(0.0), + BindingTarget::Session { session_id } => { + peaks.sessions.get(session_id).copied().unwrap_or(0.0) + } + BindingTarget::Application { name, .. } => peaks + .session_matches + .iter() + .filter(|(info, _)| { + application_name_matches( + name, + ApplicationMatchInfo { + process_path: info.process_path.as_deref(), + process_name: info.process_name.as_deref(), + display_name: Some(info.display_name.as_str()), + application_key: info.application_key.as_deref(), + ..Default::default() + }, + ) + }) + .map(|(_, peak)| *peak) + .fold(0.0_f32, f32::max), + BindingTarget::Device { device_id } => { + let (kind, raw_id) = parse_device_target(device_id); + match kind { + DeviceTargetKind::Playback => peaks.devices.get(&raw_id).copied().unwrap_or(0.0), + DeviceTargetKind::Recording => 0.0, + } + } + _ => 0.0, + } +} + +pub fn reactive_led_level_for_binding( + binding: &Binding, + peaks: &AudioPeakLevels, + previous: f32, +) -> Option { + if !binding.uses_audio_reactive_led() { + return None; + } + let mut peak = 0.0_f32; + for target in binding.normalized_targets_ref() { + peak = peak.max(peak_for_target(target, peaks)); + } + Some(envelope_led_level( + previous, + peak, + binding.normalized_led_intensity(), + LED_ENVELOPE_DECAY, + )) +} + +pub fn collect_audio_peaks(audio: &dyn AudioBackend) -> AudioPeakLevels { + audio.peak_levels().unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::{BindingAction, BindingTarget}; + use crate::test_support::binding; + use std::collections::HashMap; + + #[test] + fn intensity_scales_and_clamps_peak() { + assert!((scale_led_level(0.4, 1.0) - 0.4).abs() < 0.0001); + assert!((scale_led_level(0.4, 2.0) - 0.8).abs() < 0.0001); + assert!((scale_led_level(0.8, 2.0) - 1.0).abs() < 0.0001); + assert!((scale_led_level(0.1, 0.0) - 0.0).abs() < 0.0001); + } + + #[test] + fn envelope_holds_brief_pulse_tail() { + let first = envelope_led_level(0.0, 1.0, 1.0, 0.8); + assert!((first - 1.0).abs() < 0.0001); + let second = envelope_led_level(first, 0.0, 1.0, 0.8); + assert!((second - 0.8).abs() < 0.0001); + let third = envelope_led_level(second, 0.1, 1.0, 0.8); + assert!((third - 0.64).abs() < 0.0001); + } + + #[test] + fn peak_for_master_and_application_targets() { + let peaks = AudioPeakLevels { + master: 0.55, + devices: HashMap::new(), + sessions: HashMap::from([("chrome".into(), 0.2)]), + focused_session_id: None, + session_matches: vec![( + crate::audio::SessionPeakIdentity { + display_name: "Chrome".into(), + application_key: Some("chrome".into()), + process_name: Some("chrome.exe".into()), + process_path: None, + }, + 0.7, + )], + }; + assert!((peak_for_target(&BindingTarget::Master, &peaks) - 0.55).abs() < 0.0001); + assert!( + (peak_for_target( + &BindingTarget::Application { + name: "chrome".into(), + display_name: None, + icon_data: None, + }, + &peaks + ) - 0.7) + .abs() + < 0.0001 + ); + } + + #[test] + fn reactive_level_respects_mode_and_intensity() { + let mut binding = binding(); + binding.action = BindingAction::Volume; + binding.targets = vec![BindingTarget::Master]; + binding.led_feedback_mode = crate::model::LedFeedbackMode::FollowValue; + let peaks = AudioPeakLevels { + master: 0.5, + ..AudioPeakLevels::default() + }; + assert!(reactive_led_level_for_binding(&binding, &peaks, 0.0).is_none()); + + binding.led_feedback_mode = crate::model::LedFeedbackMode::AudioReactive; + binding.led_intensity = 2.0; + let value = reactive_led_level_for_binding(&binding, &peaks, 0.0).expect("reactive"); + assert!((value - 1.0).abs() < 0.0001); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dc0023f..9b63ca7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -15,6 +15,7 @@ mod device_target; mod durable_json_store; mod fader_curve; mod feedback; +mod led_feedback; mod midi; mod midi_event_queue; mod midi_reconciliation; diff --git a/src-tauri/src/model.rs b/src-tauri/src/model.rs index d00b1e4..8dabd93 100644 --- a/src-tauri/src/model.rs +++ b/src-tauri/src/model.rs @@ -12,9 +12,9 @@ pub use binding_types::{ normalize_macro_draft_steps, normalize_macro_steps, AssignMode, AutoHotkeyScriptMapping, AuxiliaryControl, Binding, BindingAction, BindingControlKind, BindingTarget, BindingTargetFeedbackSource, ButtonLightBehavior, ButtonLightMode, FaderCurve, FaderCurvePoint, - HotkeyMapping, MacroActionState, MacroActionStep, MacroStep, MidiMode, MuteBehavior, - OpenApplicationMapping, RelativeFormat, SoundboardMapping, MACRO_MAX_PARALLEL_STEPS, - MACRO_MAX_TOP_LEVEL_STEPS, MACRO_MAX_WAIT_MS, + HotkeyMapping, LedFeedbackMode, MacroActionState, MacroActionStep, MacroStep, MidiMode, + MuteBehavior, OpenApplicationMapping, RelativeFormat, SoundboardMapping, + MACRO_MAX_PARALLEL_STEPS, MACRO_MAX_TOP_LEVEL_STEPS, MACRO_MAX_WAIT_MS, }; #[allow(unused_imports)] pub use midi_types::{ diff --git a/src-tauri/src/model/binding_types.rs b/src-tauri/src/model/binding_types.rs index e3e2e73..1e6a4b4 100644 --- a/src-tauri/src/model/binding_types.rs +++ b/src-tauri/src/model/binding_types.rs @@ -1,5 +1,5 @@ pub use super::action_mappings::*; -use super::control_types::default_feedback_enabled; +use super::control_types::{default_feedback_enabled, default_led_intensity}; pub use super::control_types::*; pub use super::macro_types::*; use super::midi_types::{MidiControl, MidiMessageType}; @@ -41,6 +41,10 @@ pub struct Binding { #[serde(default = "default_feedback_enabled")] pub feedback_enabled: bool, #[serde(default)] + pub led_feedback_mode: LedFeedbackMode, + #[serde(default = "default_led_intensity")] + pub led_intensity: f32, + #[serde(default)] pub indicator_control: Option, #[serde(default)] pub mute_control: Option, @@ -143,6 +147,17 @@ impl Binding { )) } + pub fn uses_audio_reactive_led(&self) -> bool { + self.feedback_enabled && self.led_feedback_mode.is_audio_reactive() + } + + pub fn normalized_led_intensity(&self) -> f32 { + if !self.led_intensity.is_finite() { + return 1.0; + } + self.led_intensity.clamp(0.0, 2.0) + } + pub fn mapped_button_light_feedback_value(&self) -> Option { self.mapped_button_light_feedback_value_with_availability(|_| true) } @@ -153,6 +168,7 @@ impl Binding { ) -> Option { if !self.feedback_enabled || !self.is_button_binding() + || self.uses_audio_reactive_led() || !matches!(&self.button_light_mode, ButtonLightMode::MappedWhenAssigned) { return None; @@ -207,6 +223,9 @@ impl Binding { if !self.feedback_enabled || !self.is_button_binding() { return None; } + if self.uses_audio_reactive_led() { + return None; + } if let Some(value) = self.mapped_button_light_feedback_value() { return Some(value); diff --git a/src-tauri/src/model/control_types.rs b/src-tauri/src/model/control_types.rs index 77cd519..ef0346c 100644 --- a/src-tauri/src/model/control_types.rs +++ b/src-tauri/src/model/control_types.rs @@ -80,6 +80,23 @@ pub(super) fn default_feedback_enabled() -> bool { true } +pub(super) fn default_led_intensity() -> f32 { + 1.0 +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum LedFeedbackMode { + #[default] + FollowValue, + AudioReactive, +} + +impl LedFeedbackMode { + pub fn is_audio_reactive(&self) -> bool { + matches!(self, Self::AudioReactive) + } +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum RelativeFormat { #[default] diff --git a/src-tauri/src/model/tests.rs b/src-tauri/src/model/tests.rs index 8264ad1..48e0fe1 100644 --- a/src-tauri/src/model/tests.rs +++ b/src-tauri/src/model/tests.rs @@ -639,6 +639,25 @@ fn deserialize_binding_defaults_mute_behavior_to_toggle_on_press() { assert_eq!(binding.mute_behavior, MuteBehavior::ToggleOnPress); } +#[test] +fn deserialize_binding_defaults_led_feedback_mode_to_follow_value() { + let binding: Binding = + serde_json::from_value(binding_base_json()).expect("binding should deserialize"); + assert_eq!(binding.led_feedback_mode, LedFeedbackMode::FollowValue); + assert!((binding.led_intensity - 1.0).abs() < f32::EPSILON); + assert!(!binding.uses_audio_reactive_led()); +} + +#[test] +fn audio_reactive_led_uses_intensity_clamp() { + let mut binding: Binding = + serde_json::from_value(binding_base_json()).expect("binding should deserialize"); + binding.led_feedback_mode = LedFeedbackMode::AudioReactive; + binding.led_intensity = 3.5; + assert!(binding.uses_audio_reactive_led()); + assert!((binding.normalized_led_intensity() - 2.0).abs() < f32::EPSILON); +} + #[test] fn deserialize_binding_defaults_button_light_mode_to_activity() { let binding: Binding = diff --git a/src-tauri/src/test_support.rs b/src-tauri/src/test_support.rs index a68690d..81b0c3e 100644 --- a/src-tauri/src/test_support.rs +++ b/src-tauri/src/test_support.rs @@ -26,6 +26,8 @@ pub fn binding() -> Binding { button_light_mode: model::ButtonLightMode::Activity, button_light_behavior: model::ButtonLightBehavior::FollowState, feedback_enabled: true, + led_feedback_mode: model::LedFeedbackMode::FollowValue, + led_intensity: 1.0, indicator_control: None, mute_control: None, assign_control: None, diff --git a/src/app/dom_refs.js b/src/app/dom_refs.js index c3219fd..2fb309b 100644 --- a/src/app/dom_refs.js +++ b/src/app/dom_refs.js @@ -37,6 +37,9 @@ export const DOM_REF_IDS = Object.freeze({ bindingConfigButtonLightSection: "binding-config-button-light-section", bindingConfigButtonLightSelectRow: "binding-config-button-light-select-row", bindingConfigButtonLightSelect: "binding-config-button-light-select", + bindingConfigLedIntensityRow: "binding-config-led-intensity-row", + bindingConfigLedIntensity: "binding-config-led-intensity", + bindingConfigLedIntensityValue: "binding-config-led-intensity-value", bindingConfigIndicatorCustom: "binding-config-indicator-custom", bindingConfigIndicatorMsgType: "binding-config-indicator-msg-type", bindingConfigIndicatorChannel: "binding-config-indicator-channel", @@ -78,6 +81,11 @@ export const DOM_REF_IDS = Object.freeze({ bindingConfigSoundboardVirtualMic: "binding-config-soundboard-virtual-mic", bindingConfigCurveSection: "binding-config-curve-section", bindingConfigFeedbackOutputSection: "binding-config-feedback-output-section", + bindingConfigLedModeRow: "binding-config-led-mode-row", + bindingConfigLedModeSelect: "binding-config-led-mode-select", + bindingConfigFeedbackLedIntensityRow: "binding-config-feedback-led-intensity-row", + bindingConfigFeedbackLedIntensity: "binding-config-feedback-led-intensity", + bindingConfigFeedbackLedIntensityValue: "binding-config-feedback-led-intensity-value", bindingConfigFeedbackOutputCustom: "binding-config-feedback-output-custom", bindingConfigFeedbackMsgType: "binding-config-feedback-msg-type", bindingConfigFeedbackChannel: "binding-config-feedback-channel", diff --git a/src/core/binding_model.js b/src/core/binding_model.js index 19f9c04..2c411bc 100644 --- a/src/core/binding_model.js +++ b/src/core/binding_model.js @@ -535,5 +535,8 @@ export function normalizeButtonLightFields(binding) { binding.button_light_mode = "Activity"; binding.button_light_behavior = normalizeButtonLightBehavior(binding.button_light_behavior); } + binding.led_feedback_mode = binding.led_feedback_mode === "AudioReactive" ? "AudioReactive" : "FollowValue"; + const intensity = Number(binding.led_intensity); + binding.led_intensity = Number.isFinite(intensity) ? Math.min(2, Math.max(0, intensity)) : 1; return binding; } diff --git a/src/features/bindings/bindings.js b/src/features/bindings/bindings.js index 053aba2..2ccdf93 100644 --- a/src/features/bindings/bindings.js +++ b/src/features/bindings/bindings.js @@ -373,6 +373,7 @@ export function createBindingsFeature({ syncFeedbackOutputUi, updateIndicatorFromFields, updateFeedbackOutputFromFields, + updateLedIntensityFromInput, } = createFeedbackEditor({ elements, editorState, @@ -780,6 +781,7 @@ export function createBindingsFeature({ updateCustomCurveFromPointer, updateFeedbackOutputFromFields, updateIndicatorFromFields, + updateLedIntensityFromInput, updateMacroDrag, }); diff --git a/src/features/bindings/controllers/config_events.js b/src/features/bindings/controllers/config_events.js index 1a2a041..967ba48 100644 --- a/src/features/bindings/controllers/config_events.js +++ b/src/features/bindings/controllers/config_events.js @@ -52,6 +52,7 @@ export function createConfigEvents({ updateCustomCurveFromPointer, updateFeedbackOutputFromFields, updateIndicatorFromFields, + updateLedIntensityFromInput, updateMacroDrag, }) { function bindConfigModalUi() { @@ -84,6 +85,13 @@ export function createConfigEvents({ title: t("bindings.feedbackMessageType"), }); } + if (elements.bindingConfigLedModeSelect && !listState.ledModeDropdown) { + listState.ledModeDropdown = createSelectDropdownShell({ + selectEl: elements.bindingConfigLedModeSelect, + rootClass: "binding-config-light-dropdown settings-select-dropdown", + title: t("bindings.ledMode"), + }); + } renderIndicatorDropdowns(); if (elements.bindingConfigPanel) { @@ -124,12 +132,19 @@ export function createConfigEvents({ const nextMode = elements.bindingConfigButtonLightSelect.value; if (nextMode === "Disabled") { binding.feedback_enabled = false; + binding.led_feedback_mode = "FollowValue"; + } else if (nextMode === "AudioReactive") { + binding.feedback_enabled = true; + binding.led_feedback_mode = "AudioReactive"; + if (binding.led_intensity == null) binding.led_intensity = 1; } else if (nextMode === "MappedWhenAssigned") { binding.feedback_enabled = true; + binding.led_feedback_mode = "FollowValue"; binding.button_light_mode = "MappedWhenAssigned"; binding.button_light_behavior = normalizeButtonLightBehavior(binding.button_light_behavior); } else { binding.feedback_enabled = true; + binding.led_feedback_mode = "FollowValue"; binding.button_light_mode = "Activity"; binding.button_light_behavior = normalizeButtonLightBehavior(nextMode); } @@ -138,6 +153,31 @@ export function createConfigEvents({ renderConfigPreview(); }); } + if (elements.bindingConfigLedModeSelect) { + lifetime.listen(elements.bindingConfigLedModeSelect, "change", () => { + const binding = getConfigBinding(); + if (!binding) return; + binding.led_feedback_mode = + elements.bindingConfigLedModeSelect.value === "AudioReactive" ? "AudioReactive" : "FollowValue"; + if (binding.led_feedback_mode === "AudioReactive" && binding.feedback_enabled === false) { + binding.feedback_enabled = true; + } + if (binding.led_intensity == null) binding.led_intensity = 1; + syncFeedbackOutputUi(binding); + updateAuxLearnUi(); + renderConfigPreview(); + }); + } + if (elements.bindingConfigLedIntensity) { + lifetime.listen(elements.bindingConfigLedIntensity, "input", () => { + updateLedIntensityFromInput(elements.bindingConfigLedIntensity); + }); + } + if (elements.bindingConfigFeedbackLedIntensity) { + lifetime.listen(elements.bindingConfigFeedbackLedIntensity, "input", () => { + updateLedIntensityFromInput(elements.bindingConfigFeedbackLedIntensity); + }); + } lifetime.listen(elements.bindingConfigIndicatorMsgType, "change", updateIndicatorFromFields); lifetime.listen(elements.bindingConfigIndicatorChannel, "input", updateIndicatorFromFields); lifetime.listen(elements.bindingConfigIndicatorController, "input", updateIndicatorFromFields); diff --git a/src/features/bindings/controllers/feedback_editor.js b/src/features/bindings/controllers/feedback_editor.js index 065f15a..d79c5b2 100644 --- a/src/features/bindings/controllers/feedback_editor.js +++ b/src/features/bindings/controllers/feedback_editor.js @@ -16,6 +16,9 @@ export function createFeedbackEditor({ if (value === "Disabled") { return t("bindings.feedbackDisabled"); } + if (value === "AudioReactive") { + return t("bindings.ledMusicReactive"); + } const mode = value === "MappedWhenAssigned" ? "MappedWhenAssigned" : normalizeButtonLightBehavior(value); switch (mode) { case "MappedWhenAssigned": @@ -33,9 +36,66 @@ export function createFeedbackEditor({ function buttonLightSelectValue(binding) { if (binding?.feedback_enabled === false) return "Disabled"; + if (binding?.led_feedback_mode === "AudioReactive") return "AudioReactive"; return effectiveButtonLightMode(binding); } + function normalizeLedIntensity(raw) { + const number = Number(raw); + if (!Number.isFinite(number)) return 1; + return Math.min(2, Math.max(0, number)); + } + + function ledIntensityPercent(binding) { + return Math.round(normalizeLedIntensity(binding?.led_intensity) * 100); + } + + function syncLedIntensityUi(binding, options = {}) { + const reactive = binding?.feedback_enabled !== false && binding?.led_feedback_mode === "AudioReactive"; + const buttonRow = elements.bindingConfigLedIntensityRow; + const feedbackRow = elements.bindingConfigFeedbackLedIntensityRow; + const showButton = options.forButton === true && reactive; + const showFeedback = options.forFeedback === true && reactive; + if (buttonRow) buttonRow.classList.toggle("hidden", !showButton); + if (feedbackRow) feedbackRow.classList.toggle("hidden", !showFeedback); + const percent = String(ledIntensityPercent(binding)); + if (elements.bindingConfigLedIntensity) elements.bindingConfigLedIntensity.value = percent; + if (elements.bindingConfigLedIntensityValue) elements.bindingConfigLedIntensityValue.textContent = `${percent}%`; + if (elements.bindingConfigFeedbackLedIntensity) elements.bindingConfigFeedbackLedIntensity.value = percent; + if (elements.bindingConfigFeedbackLedIntensityValue) { + elements.bindingConfigFeedbackLedIntensityValue.textContent = `${percent}%`; + } + } + + function updateLedIntensityFromInput(inputEl) { + const binding = getConfigBinding(); + if (!binding || !inputEl) return; + binding.led_intensity = normalizeLedIntensity((Number(inputEl.value) || 0) / 100); + syncLedIntensityUi(binding, { + forButton: Boolean(elements.bindingConfigButtonLightSection && !elements.bindingConfigButtonLightSection.classList.contains("hidden")), + forFeedback: Boolean(elements.bindingConfigFeedbackOutputSection && !elements.bindingConfigFeedbackOutputSection.classList.contains("hidden")), + }); + renderConfigPreview(); + } + + function syncLedModeUi(binding) { + if (elements.bindingConfigLedModeSelect) { + elements.bindingConfigLedModeSelect.value = + binding?.led_feedback_mode === "AudioReactive" ? "AudioReactive" : "FollowValue"; + } + if (listState.ledModeDropdown && elements.bindingConfigLedModeSelect) { + renderNativeSelectDropdown({ + entry: listState.ledModeDropdown, + selectEl: elements.bindingConfigLedModeSelect, + fallbackText: t("bindings.ledFollowValue"), + formatOptionText: (option) => + option.value === "AudioReactive" ? t("bindings.ledMusicReactive") : t("bindings.ledFollowValue"), + truncateDisplayLabel: false, + }); + } + syncLedIntensityUi(binding, { forFeedback: true }); + } + function renderButtonLightDropdown() { if (!listState.buttonLightDropdown || !elements.bindingConfigButtonLightSelect) return; renderNativeSelectDropdown({ @@ -179,6 +239,7 @@ export function createFeedbackEditor({ if (elements.bindingConfigIndicatorController) elements.bindingConfigIndicatorController.value = String(control?.controller ?? 0); renderIndicatorDropdowns(); + syncLedIntensityUi(binding, { forButton: true }); } function syncFeedbackOutputUi(binding, options = {}) { @@ -212,6 +273,7 @@ export function createFeedbackEditor({ feedbackDisabled, ); renderIndicatorDropdowns(); + syncLedModeUi(binding); } function updateIndicatorFromFields() { @@ -281,7 +343,10 @@ export function createFeedbackEditor({ syncFeedbackControllerInputState, syncIndicatorUi, syncFeedbackOutputUi, + syncLedModeUi, + syncLedIntensityUi, updateIndicatorFromFields, updateFeedbackOutputFromFields, + updateLedIntensityFromInput, }; } diff --git a/src/features/bindings/template.js b/src/features/bindings/template.js index c3c811f..bdfd446 100644 --- a/src/features/bindings/template.js +++ b/src/features/bindings/template.js @@ -35,8 +35,19 @@ export const bindingsTemplate = `
When On + +
Indicator output @@ -214,10 +225,30 @@ export const bindingsTemplate = `
+ +
Feedback output - Note, CC, or Pitch Bend address that receives fader feedback. + Note, CC, or Pitch Bend address that receives fader or LED feedback.
@@ -282,6 +288,12 @@ export const bindingsTemplate = `
+ + +
diff --git a/src/locales/.i18n-meta.json b/src/locales/.i18n-meta.json index c98c4a7..8bbba6e 100644 --- a/src/locales/.i18n-meta.json +++ b/src/locales/.i18n-meta.json @@ -689,7 +689,12 @@ "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da" + "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", + "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", + "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", + "bindings.testLedDone": "1faf9a55f725b69eeaea74eea513851695809fc837fad490983c5f70b4ea75b6", + "bindings.testLedFailed": "da6748dec806bdb84ed0e2bd1c1e41658063fc636e7e9c3a26169543cd0445a1" }, "es": { "bindings.feedbackDisabled": "75081b593d15cf6e631971bc6768723f593b88b172477e40ae7d363e4829816d", @@ -1381,7 +1386,12 @@ "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da" + "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", + "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", + "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", + "bindings.testLedDone": "1faf9a55f725b69eeaea74eea513851695809fc837fad490983c5f70b4ea75b6", + "bindings.testLedFailed": "da6748dec806bdb84ed0e2bd1c1e41658063fc636e7e9c3a26169543cd0445a1" }, "de": { "bindings.feedbackDisabled": "75081b593d15cf6e631971bc6768723f593b88b172477e40ae7d363e4829816d", @@ -2073,7 +2083,12 @@ "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da" + "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", + "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", + "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", + "bindings.testLedDone": "1faf9a55f725b69eeaea74eea513851695809fc837fad490983c5f70b4ea75b6", + "bindings.testLedFailed": "da6748dec806bdb84ed0e2bd1c1e41658063fc636e7e9c3a26169543cd0445a1" }, "it": { "bindings.feedbackDisabled": "75081b593d15cf6e631971bc6768723f593b88b172477e40ae7d363e4829816d", @@ -2765,7 +2780,12 @@ "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da" + "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", + "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", + "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", + "bindings.testLedDone": "1faf9a55f725b69eeaea74eea513851695809fc837fad490983c5f70b4ea75b6", + "bindings.testLedFailed": "da6748dec806bdb84ed0e2bd1c1e41658063fc636e7e9c3a26169543cd0445a1" }, "pt-BR": { "bindings.feedbackDisabled": "75081b593d15cf6e631971bc6768723f593b88b172477e40ae7d363e4829816d", @@ -3457,7 +3477,12 @@ "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da" + "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", + "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", + "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", + "bindings.testLedDone": "1faf9a55f725b69eeaea74eea513851695809fc837fad490983c5f70b4ea75b6", + "bindings.testLedFailed": "da6748dec806bdb84ed0e2bd1c1e41658063fc636e7e9c3a26169543cd0445a1" }, "nl": { "bindings.feedbackDisabled": "75081b593d15cf6e631971bc6768723f593b88b172477e40ae7d363e4829816d", @@ -4149,7 +4174,12 @@ "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da" + "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", + "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", + "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", + "bindings.testLedDone": "1faf9a55f725b69eeaea74eea513851695809fc837fad490983c5f70b4ea75b6", + "bindings.testLedFailed": "da6748dec806bdb84ed0e2bd1c1e41658063fc636e7e9c3a26169543cd0445a1" }, "pl": { "bindings.feedbackDisabled": "75081b593d15cf6e631971bc6768723f593b88b172477e40ae7d363e4829816d", @@ -4841,7 +4871,12 @@ "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da" + "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", + "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", + "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", + "bindings.testLedDone": "1faf9a55f725b69eeaea74eea513851695809fc837fad490983c5f70b4ea75b6", + "bindings.testLedFailed": "da6748dec806bdb84ed0e2bd1c1e41658063fc636e7e9c3a26169543cd0445a1" }, "ja": { "bindings.feedbackDisabled": "75081b593d15cf6e631971bc6768723f593b88b172477e40ae7d363e4829816d", @@ -5533,7 +5568,12 @@ "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da" + "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", + "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", + "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", + "bindings.testLedDone": "1faf9a55f725b69eeaea74eea513851695809fc837fad490983c5f70b4ea75b6", + "bindings.testLedFailed": "da6748dec806bdb84ed0e2bd1c1e41658063fc636e7e9c3a26169543cd0445a1" }, "ko": { "bindings.feedbackDisabled": "75081b593d15cf6e631971bc6768723f593b88b172477e40ae7d363e4829816d", @@ -6225,7 +6265,12 @@ "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da" + "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", + "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", + "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", + "bindings.testLedDone": "1faf9a55f725b69eeaea74eea513851695809fc837fad490983c5f70b4ea75b6", + "bindings.testLedFailed": "da6748dec806bdb84ed0e2bd1c1e41658063fc636e7e9c3a26169543cd0445a1" }, "zh-Hans": { "bindings.feedbackDisabled": "75081b593d15cf6e631971bc6768723f593b88b172477e40ae7d363e4829816d", @@ -6917,7 +6962,12 @@ "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da" + "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", + "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", + "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", + "bindings.testLedDone": "1faf9a55f725b69eeaea74eea513851695809fc837fad490983c5f70b4ea75b6", + "bindings.testLedFailed": "da6748dec806bdb84ed0e2bd1c1e41658063fc636e7e9c3a26169543cd0445a1" }, "settings.appearance.description": "Customize how MIDIMaster looks and feels.", "settings.appearance.themePresets": "Theme Presets", diff --git a/src/locales/de.json b/src/locales/de.json index 144566d..97e710f 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -690,5 +690,10 @@ "bindings.ledFollowValue": "Follow Value", "bindings.ledMusicReactive": "Music Reactive", "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks." + "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.testLed": "Test LED", + "bindings.testLedHelp": "Flash the configured LED address three times.", + "bindings.testLedTesting": "Flashing LED…", + "bindings.testLedDone": "Test pulse sent.", + "bindings.testLedFailed": "LED test failed: {error}" } diff --git a/src/locales/en.json b/src/locales/en.json index 79a6bbb..be7f61d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -168,6 +168,11 @@ "bindings.pitchBend": "Pitch Bend", "bindings.learnFeedbackOutput": "Learn feedback output", "bindings.resetFeedbackOutput": "Reset feedback output", + "bindings.testLed": "Test LED", + "bindings.testLedHelp": "Flash the configured LED address three times.", + "bindings.testLedTesting": "Flashing LED…", + "bindings.testLedDone": "Test pulse sent.", + "bindings.testLedFailed": "LED test failed: {error}", "bindings.lightStateControlledHelp": "This button shows the target state.", "bindings.lightUnavailableHelp": "Choose a stateless button action to use mapped lighting.", "bindings.mute": "Mute", diff --git a/src/locales/es.json b/src/locales/es.json index f451ea9..29003dd 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -690,5 +690,10 @@ "bindings.ledFollowValue": "Follow Value", "bindings.ledMusicReactive": "Music Reactive", "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks." + "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.testLed": "Test LED", + "bindings.testLedHelp": "Flash the configured LED address three times.", + "bindings.testLedTesting": "Flashing LED…", + "bindings.testLedDone": "Test pulse sent.", + "bindings.testLedFailed": "LED test failed: {error}" } diff --git a/src/locales/fr.json b/src/locales/fr.json index f8bb629..2173f9a 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -690,5 +690,10 @@ "bindings.ledFollowValue": "Follow Value", "bindings.ledMusicReactive": "Music Reactive", "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks." + "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.testLed": "Test LED", + "bindings.testLedHelp": "Flash the configured LED address three times.", + "bindings.testLedTesting": "Flashing LED…", + "bindings.testLedDone": "Test pulse sent.", + "bindings.testLedFailed": "LED test failed: {error}" } diff --git a/src/locales/it.json b/src/locales/it.json index b14dcc1..4615eb6 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -690,5 +690,10 @@ "bindings.ledFollowValue": "Follow Value", "bindings.ledMusicReactive": "Music Reactive", "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks." + "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.testLed": "Test LED", + "bindings.testLedHelp": "Flash the configured LED address three times.", + "bindings.testLedTesting": "Flashing LED…", + "bindings.testLedDone": "Test pulse sent.", + "bindings.testLedFailed": "LED test failed: {error}" } diff --git a/src/locales/ja.json b/src/locales/ja.json index 61e08a3..ff551c5 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -690,5 +690,10 @@ "bindings.ledFollowValue": "Follow Value", "bindings.ledMusicReactive": "Music Reactive", "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks." + "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.testLed": "Test LED", + "bindings.testLedHelp": "Flash the configured LED address three times.", + "bindings.testLedTesting": "Flashing LED…", + "bindings.testLedDone": "Test pulse sent.", + "bindings.testLedFailed": "LED test failed: {error}" } diff --git a/src/locales/ko.json b/src/locales/ko.json index 8571e3a..4b9891b 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -690,5 +690,10 @@ "bindings.ledFollowValue": "Follow Value", "bindings.ledMusicReactive": "Music Reactive", "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks." + "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.testLed": "Test LED", + "bindings.testLedHelp": "Flash the configured LED address three times.", + "bindings.testLedTesting": "Flashing LED…", + "bindings.testLedDone": "Test pulse sent.", + "bindings.testLedFailed": "LED test failed: {error}" } diff --git a/src/locales/nl.json b/src/locales/nl.json index dd87116..1c4917a 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -690,5 +690,10 @@ "bindings.ledFollowValue": "Follow Value", "bindings.ledMusicReactive": "Music Reactive", "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks." + "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.testLed": "Test LED", + "bindings.testLedHelp": "Flash the configured LED address three times.", + "bindings.testLedTesting": "Flashing LED…", + "bindings.testLedDone": "Test pulse sent.", + "bindings.testLedFailed": "LED test failed: {error}" } diff --git a/src/locales/pl.json b/src/locales/pl.json index bf66acd..7484fb0 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -690,5 +690,10 @@ "bindings.ledFollowValue": "Follow Value", "bindings.ledMusicReactive": "Music Reactive", "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks." + "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.testLed": "Test LED", + "bindings.testLedHelp": "Flash the configured LED address three times.", + "bindings.testLedTesting": "Flashing LED…", + "bindings.testLedDone": "Test pulse sent.", + "bindings.testLedFailed": "LED test failed: {error}" } diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index 8d1daa0..a382c08 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -690,5 +690,10 @@ "bindings.ledFollowValue": "Follow Value", "bindings.ledMusicReactive": "Music Reactive", "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks." + "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.testLed": "Test LED", + "bindings.testLedHelp": "Flash the configured LED address three times.", + "bindings.testLedTesting": "Flashing LED…", + "bindings.testLedDone": "Test pulse sent.", + "bindings.testLedFailed": "LED test failed: {error}" } diff --git a/src/locales/zh-Hans.json b/src/locales/zh-Hans.json index 52f234f..32f9d0f 100644 --- a/src/locales/zh-Hans.json +++ b/src/locales/zh-Hans.json @@ -690,5 +690,10 @@ "bindings.ledFollowValue": "Follow Value", "bindings.ledMusicReactive": "Music Reactive", "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks." + "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.testLed": "Test LED", + "bindings.testLedHelp": "Flash the configured LED address three times.", + "bindings.testLedTesting": "Flashing LED…", + "bindings.testLedDone": "Test pulse sent.", + "bindings.testLedFailed": "LED test failed: {error}" } diff --git a/src/styles/bindings/config-panel/feedback-controls.css b/src/styles/bindings/config-panel/feedback-controls.css index 5e3c4f3..16d3b0f 100644 --- a/src/styles/bindings/config-panel/feedback-controls.css +++ b/src/styles/bindings/config-panel/feedback-controls.css @@ -168,6 +168,38 @@ height: 40px; } +.binding-config-feedback-test-row { + grid-column: 1 / -1; + display: flex; + align-items: center; + gap: 12px; + min-width: 0; + padding-top: 4px; +} + +.binding-config-feedback-test { + min-width: 110px; + height: 40px; + padding: 0 14px; + font-weight: 700; +} + +.binding-config-feedback-test.is-testing { + opacity: 0.72; +} + +.binding-config-feedback-test-status { + min-width: 0; + color: #93a5c4; + font-size: 12px; + font-weight: 600; + line-height: 1.35; +} + +.binding-config-feedback-test-status.is-error { + color: #ff8f8f; +} + @media (max-width: 720px) { .binding-config-indicator-custom { grid-template-columns: minmax(104px, 1fr) minmax(68px, 0.7fr) minmax(78px, 0.8fr) 40px 40px; From b226f1e474aba2f5fa376999095694308837e02f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 10:47:22 +0000 Subject: [PATCH 6/9] fix(midi): Fix Test LED IPC args and config footer layout Send camelCase deviceId/msgType to test_midi_feedback_output, and move the Test LED row out of the tight feedback mapping grid so the button and Save footer no longer collapse. Co-authored-by: Pezbino --- .../bindings/controllers/feedback_editor.js | 9 ++++--- src/features/bindings/template.js | 24 +++++++++---------- .../config-panel/feedback-controls.css | 21 +++++++++++++--- .../config-panel/preview-and-layout.css | 2 ++ .../config-panel/theme-and-responsive.css | 4 ++++ 5 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/features/bindings/controllers/feedback_editor.js b/src/features/bindings/controllers/feedback_editor.js index dcdad44..168dcb6 100644 --- a/src/features/bindings/controllers/feedback_editor.js +++ b/src/features/bindings/controllers/feedback_editor.js @@ -401,11 +401,12 @@ export function createFeedbackEditor({ setLedTestStatus("info", t("bindings.testLedTesting")); if (ledTestTimer) window.clearTimeout(ledTestTimer); try { + // Tauri command args are camelCase on the wire (device_id → deviceId). await invoke("test_midi_feedback_output", { - device_id: deviceId, + deviceId, channel, controller, - msg_type: msgType, + msgType, }); setLedTestStatus("info", t("bindings.testLedDone")); ledTestTimer = window.setTimeout(() => { @@ -413,7 +414,9 @@ export function createFeedbackEditor({ ledTestTimer = 0; }, 1800); } catch (error) { - const message = error?.message || String(error || "Unknown error"); + const raw = error?.message || String(error || "Unknown error"); + // Prefer the human-readable tail of Tauri IPC errors over the full arg dump. + const message = raw.includes(": ") ? raw.slice(raw.lastIndexOf(": ") + 2).trim() || raw : raw; setLedTestStatus("error", t("bindings.testLedFailed", { error: message })); } finally { // Keep the button busy for the full pulse sequence so users don't stack tests. diff --git a/src/features/bindings/template.js b/src/features/bindings/template.js index 7761dee..837b720 100644 --- a/src/features/bindings/template.js +++ b/src/features/bindings/template.js @@ -83,12 +83,12 @@ export const bindingsTemplate = `
- - -
+
+
+ +
@@ -288,12 +288,12 @@ export const bindingsTemplate = `
- - -
+
+
+ +
diff --git a/src/styles/bindings/config-panel/feedback-controls.css b/src/styles/bindings/config-panel/feedback-controls.css index 16d3b0f..92327cd 100644 --- a/src/styles/bindings/config-panel/feedback-controls.css +++ b/src/styles/bindings/config-panel/feedback-controls.css @@ -169,19 +169,22 @@ } .binding-config-feedback-test-row { - grid-column: 1 / -1; display: flex; align-items: center; gap: 12px; + width: 100%; min-width: 0; - padding-top: 4px; + padding-top: 2px; } .binding-config-feedback-test { + flex: 0 0 auto; + width: auto; min-width: 110px; height: 40px; padding: 0 14px; font-weight: 700; + white-space: nowrap; } .binding-config-feedback-test.is-testing { @@ -189,17 +192,24 @@ } .binding-config-feedback-test-status { + flex: 1 1 auto; min-width: 0; color: #93a5c4; font-size: 12px; font-weight: 600; line-height: 1.35; + overflow-wrap: anywhere; } .binding-config-feedback-test-status.is-error { color: #ff8f8f; } +.binding-config-section--feedback-output .binding-config-feedback-test-row, +.binding-config-section--button-light .binding-config-feedback-test-row { + margin-top: 2px; +} + @media (max-width: 720px) { .binding-config-indicator-custom { grid-template-columns: minmax(104px, 1fr) minmax(68px, 0.7fr) minmax(78px, 0.8fr) 40px 40px; @@ -210,8 +220,13 @@ } } +.binding-config-section--feedback-output { + align-content: start; + min-width: 0; +} + .binding-config-led-intensity-row { - margin-top: 10px; + margin-top: 0; } .binding-config-led-intensity-control { diff --git a/src/styles/bindings/config-panel/preview-and-layout.css b/src/styles/bindings/config-panel/preview-and-layout.css index 6e4e180..1023205 100644 --- a/src/styles/bindings/config-panel/preview-and-layout.css +++ b/src/styles/bindings/config-panel/preview-and-layout.css @@ -396,6 +396,8 @@ .binding-config-panel--fader:not(.binding-config-panel--macro-page) .binding-config-section--feedback-output { grid-area: feedback; + min-width: 0; + overflow: hidden; } .binding-config-panel--fader:not(.binding-config-panel--macro-page) .binding-config-section--mute { diff --git a/src/styles/bindings/config-panel/theme-and-responsive.css b/src/styles/bindings/config-panel/theme-and-responsive.css index 51b28ba..012e193 100644 --- a/src/styles/bindings/config-panel/theme-and-responsive.css +++ b/src/styles/bindings/config-panel/theme-and-responsive.css @@ -246,16 +246,20 @@ body[data-theme] .binding-config-midi-control { .binding-config-footer { display: flex; justify-content: flex-end; + flex-wrap: nowrap; gap: 8px; padding: 8px 10px 10px; border-top: 1px solid rgba(122, 141, 179, 0.16); flex: 0 0 auto; + min-width: 0; } .binding-config-footer button { + flex: 0 0 auto; min-width: 136px; height: 34px; border-radius: 10px; + white-space: nowrap; } @media (max-height: 780px) { From 4e67b68ab566e6abd3572fa3ccc4ee1e812a8014 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 10:53:40 +0000 Subject: [PATCH 7/9] fix(midi): Stop clipping feedback fields in Music Reactive mode overflow:hidden on the fader feedback section zeroed its grid min-size and hid Type/Channel/Control and Test LED once the intensity row appeared. Co-authored-by: Pezbino --- src/styles/bindings/config-panel/preview-and-layout.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/styles/bindings/config-panel/preview-and-layout.css b/src/styles/bindings/config-panel/preview-and-layout.css index 1023205..28276a0 100644 --- a/src/styles/bindings/config-panel/preview-and-layout.css +++ b/src/styles/bindings/config-panel/preview-and-layout.css @@ -397,7 +397,8 @@ .binding-config-panel--fader:not(.binding-config-panel--macro-page) .binding-config-section--feedback-output { grid-area: feedback; min-width: 0; - overflow: hidden; + /* Keep overflow visible so Music Reactive intensity does not clip Type/Channel/Control/Test. */ + overflow: visible; } .binding-config-panel--fader:not(.binding-config-panel--macro-page) .binding-config-section--mute { From a7fd78772b35cc1a95057838309153e66f8c6852 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 11:03:00 +0000 Subject: [PATCH 8/9] fix(midi): Pin Save footer so fader config no longer floats Stop the fader layout from shrinking under taller LED controls, scroll the body instead, and give the Cancel/Save bar a solid background. Co-authored-by: Pezbino --- src/styles/bindings/config-panel/preview-and-layout.css | 8 ++++---- src/styles/bindings/config-panel/theme-and-responsive.css | 4 ++++ src/styles/settings/theme-overrides.css | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/styles/bindings/config-panel/preview-and-layout.css b/src/styles/bindings/config-panel/preview-and-layout.css index 28276a0..1189e9e 100644 --- a/src/styles/bindings/config-panel/preview-and-layout.css +++ b/src/styles/bindings/config-panel/preview-and-layout.css @@ -339,7 +339,8 @@ .binding-config-panel--fader:not(.binding-config-panel--macro-page) .binding-config-layout { display: grid; - flex: 0 1 auto; + /* Keep content height; body scrolls. Shrinking + overflow:visible painted over the Save footer. */ + flex: 0 0 auto; grid-template-columns: minmax(0, 0.675fr) minmax(0, 0.675fr) minmax(270px, 0.85fr); grid-template-areas: "name name live" @@ -350,7 +351,7 @@ align-items: stretch; align-content: start; padding: 10px; - overflow: visible; + overflow: hidden; } .binding-config-panel--fader:not(.binding-config-panel--macro-page) .binding-config-main-column { @@ -397,8 +398,7 @@ .binding-config-panel--fader:not(.binding-config-panel--macro-page) .binding-config-section--feedback-output { grid-area: feedback; min-width: 0; - /* Keep overflow visible so Music Reactive intensity does not clip Type/Channel/Control/Test. */ - overflow: visible; + min-height: min-content; } .binding-config-panel--fader:not(.binding-config-panel--macro-page) .binding-config-section--mute { diff --git a/src/styles/bindings/config-panel/theme-and-responsive.css b/src/styles/bindings/config-panel/theme-and-responsive.css index 012e193..2cfb985 100644 --- a/src/styles/bindings/config-panel/theme-and-responsive.css +++ b/src/styles/bindings/config-panel/theme-and-responsive.css @@ -247,11 +247,15 @@ body[data-theme] .binding-config-midi-control { display: flex; justify-content: flex-end; flex-wrap: nowrap; + align-items: center; gap: 8px; padding: 8px 10px 10px; border-top: 1px solid rgba(122, 141, 179, 0.16); flex: 0 0 auto; min-width: 0; + position: relative; + z-index: 2; + background: rgba(13, 20, 31, 0.98); } .binding-config-footer button { diff --git a/src/styles/settings/theme-overrides.css b/src/styles/settings/theme-overrides.css index aae17cb..873c05f 100644 --- a/src/styles/settings/theme-overrides.css +++ b/src/styles/settings/theme-overrides.css @@ -401,6 +401,7 @@ body[data-theme] #binding-config-panel .binding-config-layout { body[data-theme] #binding-config-panel .binding-config-main-column, body[data-theme] #binding-config-panel .binding-config-footer { border-color: var(--binding-config-border-soft); + background: var(--binding-config-panel-bg); } body[data-theme] #binding-config-panel .binding-config-section, From 20b3315c09a1e0733e9f9464b6bf2c85c712f210 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 11:11:25 +0000 Subject: [PATCH 9/9] feat(midi): Make audio-reactive LED sensitivity punchier Redesign intensity as a nonlinear sensitivity curve with adaptive baseline punch so mid-metering levels still flash when the slider is raised, and rename Music Reactive to Audio Reactive in the UI. Co-authored-by: Pezbino --- docs/USER_GUIDE.md | 10 +-- src-tauri/src/background_tasks.rs | 22 +++-- src-tauri/src/led_feedback.rs | 142 +++++++++++++++++++++++------- src/features/bindings/template.js | 16 ++-- src/locales/.i18n-meta.json | 67 +++++++------- src/locales/de.json | 6 +- src/locales/en.json | 6 +- src/locales/es.json | 6 +- src/locales/fr.json | 6 +- src/locales/it.json | 6 +- src/locales/ja.json | 6 +- src/locales/ko.json | 6 +- src/locales/nl.json | 6 +- src/locales/pl.json | 6 +- src/locales/pt-BR.json | 6 +- src/locales/zh-Hans.json | 6 +- 16 files changed, 205 insertions(+), 118 deletions(-) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index f9c15aa..625ba5f 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -17,16 +17,16 @@ MIDIMaster binds controls on your MIDI device (faders, knobs, buttons) to target When a target changes, MIDIMaster can send feedback back to your MIDI controller (for motorized faders, LEDs, etc.). -### Music-reactive LEDs +### Audio-reactive LEDs -For fader and button bindings, you can switch LED feedback to **Music Reactive** mode: +For fader and button bindings, you can switch LED feedback to **Audio Reactive** mode: 1. Open the binding configuration. -2. For faders/knobs, set **LED mode** to **Music Reactive**. For buttons, choose **Music Reactive** in the button light list. -3. Adjust **LED intensity** so quiet audio stays dim and louder peaks flash brighter. +2. For faders/knobs, set **LED mode** to **Audio Reactive**. For buttons, choose **Audio Reactive** in the button light list. +3. Adjust **LED sensitivity** so the light responds more (or less) to peaks and small level changes around the current meter. 4. Optionally learn or type a custom Note/CC/Pitch Bend address for the LED output (already supported under Feedback output / Indicator output). -In Music Reactive mode the light follows live audio peaks for the bound target (master, device, application, session, or focused app). Quiet passages stay dark; louder beats drive brighter pulses. If a fader also has a custom LED address, motor fader position feedback still updates the primary control while the LED address receives the reactive signal. +In Audio Reactive mode the light follows live audio peaks for the bound target (master, device, application, session, or focused app). Quiet passages stay dark; louder beats and short rises above the recent level drive brighter pulses. Higher sensitivity makes those mid-metering fluctuations more visible. If a fader also has a custom LED address, motor fader position feedback still updates the primary control while the LED address receives the reactive signal. ## First-Time Setup diff --git a/src-tauri/src/background_tasks.rs b/src-tauri/src/background_tasks.rs index 26bb4c6..26bc9ff 100644 --- a/src-tauri/src/background_tasks.rs +++ b/src-tauri/src/background_tasks.rs @@ -264,7 +264,8 @@ pub(crate) fn spawn_feedback_refresh_loop( let mut last_reactive_led_sync = Instant::now() .checked_sub(AUDIO_REACTIVE_LED_INTERVAL) .unwrap_or_else(Instant::now); - let mut reactive_led_levels: HashMap = HashMap::new(); + let mut reactive_led_state: HashMap = + HashMap::new(); loop { if shutdown_requested(&shutdown) { @@ -339,7 +340,7 @@ pub(crate) fn spawn_feedback_refresh_loop( if context_changed { last_feedback_context = context; last_sent_feedback.clear(); - reactive_led_levels.clear(); + reactive_led_state.clear(); } let force_feedback_resend = context_changed @@ -443,13 +444,18 @@ pub(crate) fn spawn_feedback_refresh_loop( let output_control = feedback::binding_feedback_control_key(binding); let output_key = output_control.to_binding_key(); - let previous = reactive_led_levels.get(&output_key).copied().unwrap_or(0.0); - let Some(level) = crate::led_feedback::reactive_led_level_for_binding( - binding, &peaks, previous, - ) else { + let previous = reactive_led_state + .get(&output_key) + .copied() + .unwrap_or_default(); + let Some((level, next_state)) = + crate::led_feedback::reactive_led_level_for_binding( + binding, &peaks, previous, + ) + else { continue; }; - reactive_led_levels.insert(output_key.clone(), level); + reactive_led_state.insert(output_key.clone(), next_state); if should_send_feedback( &mut last_sent_feedback, output_key, @@ -469,7 +475,7 @@ pub(crate) fn spawn_feedback_refresh_loop( last_focused_session = None; last_feedback_context.clear(); last_sent_feedback.clear(); - reactive_led_levels.clear(); + reactive_led_state.clear(); } if last_osd_hide_check.elapsed() >= OSD_HIDE_CHECK_INTERVAL { diff --git a/src-tauri/src/led_feedback.rs b/src-tauri/src/led_feedback.rs index faac87c..2fa4569 100644 --- a/src-tauri/src/led_feedback.rs +++ b/src-tauri/src/led_feedback.rs @@ -1,37 +1,92 @@ use crate::audio::target_match::{application_name_matches, ApplicationMatchInfo}; -use crate::audio::{AudioPeakLevels, AudioBackend}; +use crate::audio::{AudioBackend, AudioPeakLevels}; use crate::device_target::{parse_device_target, DeviceTargetKind}; use crate::model::{Binding, BindingTarget}; -pub const LED_ENVELOPE_DECAY: f32 = 0.82; +/// Decay applied to the previous LED level so flashes have a short visible tail. +pub const LED_ENVELOPE_DECAY: f32 = 0.78; +/// How quickly the adaptive baseline tracks steady metering (per ~40ms tick). +pub const LED_BASELINE_ALPHA: f32 = 0.08; -pub fn scale_led_level(peak: f32, intensity: f32) -> f32 { - let peak = if peak.is_finite() { - peak.clamp(0.0, 1.0) - } else { - 0.0 - }; - let intensity = if intensity.is_finite() { +#[derive(Debug, Clone, Copy, Default)] +pub struct ReactiveLedState { + pub level: f32, + pub baseline: f32, +} + +pub fn normalize_intensity(intensity: f32) -> f32 { + if intensity.is_finite() { intensity.clamp(0.0, 2.0) } else { 1.0 - }; - (peak * intensity).clamp(0.0, 1.0) + } } -pub fn envelope_led_level(previous: f32, peak: f32, intensity: f32, decay: f32) -> f32 { - let next = scale_led_level(peak, intensity); - let previous = if previous.is_finite() { - previous.clamp(0.0, 1.0) +pub fn normalize_peak(peak: f32) -> f32 { + if peak.is_finite() { + peak.clamp(0.0, 1.0) } else { 0.0 + } +} + +/// Map sensitivity (0..=2) onto a shaped LED drive for a single peak sample. +/// +/// Higher sensitivity: +/// - lifts mid/quiet peaks with a softer gamma curve +/// - amplifies short rises above a slow baseline so steady metering still flashes +pub fn scale_led_level(peak: f32, intensity: f32) -> f32 { + let peak = normalize_peak(peak); + let intensity = normalize_intensity(intensity); + if intensity <= 0.0 || peak <= 0.0 { + return 0.0; + } + // intensity 1 → near-linear; 2 → strong lift of quieter peaks; low → darker. + let gamma = (1.15 / intensity.max(0.12)).clamp(0.32, 3.8); + let shaped = peak.powf(gamma); + let gain = 0.55 + intensity * 0.85; + (shaped * gain).clamp(0.0, 1.0) +} + +/// Adaptive response: absolute shaped level + punch for peaks above the rolling baseline. +pub fn sensitive_led_drive(peak: f32, baseline: f32, intensity: f32) -> (f32, f32) { + let peak = normalize_peak(peak); + let intensity = normalize_intensity(intensity); + let baseline = normalize_peak(baseline); + + let next_baseline = if intensity <= 0.0 { + 0.0 + } else if baseline <= 0.001 { + peak + } else { + baseline + (peak - baseline) * LED_BASELINE_ALPHA }; + + if intensity <= 0.0 { + return (0.0, next_baseline); + } + + let absolute = scale_led_level(peak, intensity); + // Excess above the recent average — this is what makes steady loud music still pulse. + let delta = (peak - next_baseline).max(0.0); + // Punch grows with the slider: ~100% is moderate, ~200% catches small mid-meter bumps. + let punch_gain = 1.0 + intensity * 2.5 + intensity * intensity * 3.5; + let punch = (delta * punch_gain).clamp(0.0, 1.0); + // Blend: higher sensitivity leans more on punch so micro-dynamics dominate. + let punch_mix = (0.3 + intensity * 0.35).clamp(0.3, 1.0); + let driven = (absolute * (1.0 - punch_mix * 0.5) + punch * punch_mix).clamp(0.0, 1.0); + (driven, next_baseline) +} + +pub fn envelope_led_level(previous: f32, drive: f32, decay: f32) -> f32 { + let drive = normalize_peak(drive); + let previous = normalize_peak(previous); let decay = if decay.is_finite() { decay.clamp(0.0, 1.0) } else { LED_ENVELOPE_DECAY }; - next.max(previous * decay) + drive.max(previous * decay) } pub fn peak_for_target(target: &BindingTarget, peaks: &AudioPeakLevels) -> f32 { @@ -81,8 +136,8 @@ pub fn peak_for_target(target: &BindingTarget, peaks: &AudioPeakLevels) -> f32 { pub fn reactive_led_level_for_binding( binding: &Binding, peaks: &AudioPeakLevels, - previous: f32, -) -> Option { + state: ReactiveLedState, +) -> Option<(f32, ReactiveLedState)> { if !binding.uses_audio_reactive_led() { return None; } @@ -90,11 +145,15 @@ pub fn reactive_led_level_for_binding( for target in binding.normalized_targets_ref() { peak = peak.max(peak_for_target(target, peaks)); } - Some(envelope_led_level( - previous, - peak, - binding.normalized_led_intensity(), - LED_ENVELOPE_DECAY, + let intensity = binding.normalized_led_intensity(); + let (drive, baseline) = sensitive_led_drive(peak, state.baseline, intensity); + let level = envelope_led_level(state.level, drive, LED_ENVELOPE_DECAY); + Some(( + level, + ReactiveLedState { + level, + baseline, + }, )) } @@ -110,20 +169,34 @@ mod tests { use std::collections::HashMap; #[test] - fn intensity_scales_and_clamps_peak() { - assert!((scale_led_level(0.4, 1.0) - 0.4).abs() < 0.0001); - assert!((scale_led_level(0.4, 2.0) - 0.8).abs() < 0.0001); - assert!((scale_led_level(0.8, 2.0) - 1.0).abs() < 0.0001); + fn intensity_lifts_mid_peaks_non_linearly() { + let linearish = scale_led_level(0.35, 1.0); + let sensitive = scale_led_level(0.35, 2.0); + assert!(sensitive > linearish + 0.08); assert!((scale_led_level(0.1, 0.0) - 0.0).abs() < 0.0001); + assert!(scale_led_level(0.8, 2.0) >= 0.95); + } + + #[test] + fn sensitivity_amplifies_rises_above_steady_meter() { + // Steady metering around 0.45, then a small bump — high sensitivity should flash harder. + let baseline = 0.45; + let bump = 0.52; + let (_, next_base_low) = sensitive_led_drive(bump, baseline, 0.5); + let (low, _) = sensitive_led_drive(bump, baseline, 0.5); + let (high, next_base_high) = sensitive_led_drive(bump, baseline, 2.0); + assert!(high > low + 0.2); + assert!(next_base_low > 0.0); + assert!(next_base_high > 0.0); } #[test] fn envelope_holds_brief_pulse_tail() { - let first = envelope_led_level(0.0, 1.0, 1.0, 0.8); + let first = envelope_led_level(0.0, 1.0, 0.8); assert!((first - 1.0).abs() < 0.0001); - let second = envelope_led_level(first, 0.0, 1.0, 0.8); + let second = envelope_led_level(first, 0.0, 0.8); assert!((second - 0.8).abs() < 0.0001); - let third = envelope_led_level(second, 0.1, 1.0, 0.8); + let third = envelope_led_level(second, 0.1, 0.8); assert!((third - 0.64).abs() < 0.0001); } @@ -169,11 +242,14 @@ mod tests { master: 0.5, ..AudioPeakLevels::default() }; - assert!(reactive_led_level_for_binding(&binding, &peaks, 0.0).is_none()); + assert!(reactive_led_level_for_binding(&binding, &peaks, ReactiveLedState::default()).is_none()); binding.led_feedback_mode = crate::model::LedFeedbackMode::AudioReactive; binding.led_intensity = 2.0; - let value = reactive_led_level_for_binding(&binding, &peaks, 0.0).expect("reactive"); - assert!((value - 1.0).abs() < 0.0001); + let (value, state) = + reactive_led_level_for_binding(&binding, &peaks, ReactiveLedState::default()) + .expect("reactive"); + assert!(value > 0.4); + assert!(state.baseline > 0.0); } } diff --git a/src/features/bindings/template.js b/src/features/bindings/template.js index 837b720..e01f8cf 100644 --- a/src/features/bindings/template.js +++ b/src/features/bindings/template.js @@ -35,16 +35,16 @@ export const bindingsTemplate = `
When On - + @@ -238,16 +238,16 @@ export const bindingsTemplate = `
- + diff --git a/src/locales/.i18n-meta.json b/src/locales/.i18n-meta.json index 8bbba6e..032dd42 100644 --- a/src/locales/.i18n-meta.json +++ b/src/locales/.i18n-meta.json @@ -687,9 +687,9 @@ "bindings.ledMode": "8b797e51f1310507dcdfef4cf8dc8db164afe34a3ad5595e454bb0b24e6c18cb", "bindings.ledModeHelp": "e1fdb51d3321ded27e44b28d52acea95e94aa578e23fcd3dde0e8590322e94b1", "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", - "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", - "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.ledMusicReactive": "4187beb90a5e4423eb405f60ad6849231d34f76f1c8bdcd4eef222460282c18a", + "bindings.ledIntensity": "37aede6ebb44595af3d3f6a85cf90c3c8430de5a66de6e503de1e047ae3397a4", + "bindings.ledIntensityHelp": "644d876e9a276e7ff74d0d4eeee5351ab05a462f2325f8e7d4ae3697111e6659", "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", @@ -1384,9 +1384,9 @@ "bindings.ledMode": "8b797e51f1310507dcdfef4cf8dc8db164afe34a3ad5595e454bb0b24e6c18cb", "bindings.ledModeHelp": "e1fdb51d3321ded27e44b28d52acea95e94aa578e23fcd3dde0e8590322e94b1", "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", - "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", - "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.ledMusicReactive": "4187beb90a5e4423eb405f60ad6849231d34f76f1c8bdcd4eef222460282c18a", + "bindings.ledIntensity": "37aede6ebb44595af3d3f6a85cf90c3c8430de5a66de6e503de1e047ae3397a4", + "bindings.ledIntensityHelp": "644d876e9a276e7ff74d0d4eeee5351ab05a462f2325f8e7d4ae3697111e6659", "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", @@ -2081,9 +2081,9 @@ "bindings.ledMode": "8b797e51f1310507dcdfef4cf8dc8db164afe34a3ad5595e454bb0b24e6c18cb", "bindings.ledModeHelp": "e1fdb51d3321ded27e44b28d52acea95e94aa578e23fcd3dde0e8590322e94b1", "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", - "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", - "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.ledMusicReactive": "4187beb90a5e4423eb405f60ad6849231d34f76f1c8bdcd4eef222460282c18a", + "bindings.ledIntensity": "37aede6ebb44595af3d3f6a85cf90c3c8430de5a66de6e503de1e047ae3397a4", + "bindings.ledIntensityHelp": "644d876e9a276e7ff74d0d4eeee5351ab05a462f2325f8e7d4ae3697111e6659", "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", @@ -2778,9 +2778,9 @@ "bindings.ledMode": "8b797e51f1310507dcdfef4cf8dc8db164afe34a3ad5595e454bb0b24e6c18cb", "bindings.ledModeHelp": "e1fdb51d3321ded27e44b28d52acea95e94aa578e23fcd3dde0e8590322e94b1", "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", - "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", - "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.ledMusicReactive": "4187beb90a5e4423eb405f60ad6849231d34f76f1c8bdcd4eef222460282c18a", + "bindings.ledIntensity": "37aede6ebb44595af3d3f6a85cf90c3c8430de5a66de6e503de1e047ae3397a4", + "bindings.ledIntensityHelp": "644d876e9a276e7ff74d0d4eeee5351ab05a462f2325f8e7d4ae3697111e6659", "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", @@ -3475,9 +3475,9 @@ "bindings.ledMode": "8b797e51f1310507dcdfef4cf8dc8db164afe34a3ad5595e454bb0b24e6c18cb", "bindings.ledModeHelp": "e1fdb51d3321ded27e44b28d52acea95e94aa578e23fcd3dde0e8590322e94b1", "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", - "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", - "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.ledMusicReactive": "4187beb90a5e4423eb405f60ad6849231d34f76f1c8bdcd4eef222460282c18a", + "bindings.ledIntensity": "37aede6ebb44595af3d3f6a85cf90c3c8430de5a66de6e503de1e047ae3397a4", + "bindings.ledIntensityHelp": "644d876e9a276e7ff74d0d4eeee5351ab05a462f2325f8e7d4ae3697111e6659", "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", @@ -4172,9 +4172,9 @@ "bindings.ledMode": "8b797e51f1310507dcdfef4cf8dc8db164afe34a3ad5595e454bb0b24e6c18cb", "bindings.ledModeHelp": "e1fdb51d3321ded27e44b28d52acea95e94aa578e23fcd3dde0e8590322e94b1", "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", - "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", - "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.ledMusicReactive": "4187beb90a5e4423eb405f60ad6849231d34f76f1c8bdcd4eef222460282c18a", + "bindings.ledIntensity": "37aede6ebb44595af3d3f6a85cf90c3c8430de5a66de6e503de1e047ae3397a4", + "bindings.ledIntensityHelp": "644d876e9a276e7ff74d0d4eeee5351ab05a462f2325f8e7d4ae3697111e6659", "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", @@ -4869,9 +4869,9 @@ "bindings.ledMode": "8b797e51f1310507dcdfef4cf8dc8db164afe34a3ad5595e454bb0b24e6c18cb", "bindings.ledModeHelp": "e1fdb51d3321ded27e44b28d52acea95e94aa578e23fcd3dde0e8590322e94b1", "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", - "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", - "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.ledMusicReactive": "4187beb90a5e4423eb405f60ad6849231d34f76f1c8bdcd4eef222460282c18a", + "bindings.ledIntensity": "37aede6ebb44595af3d3f6a85cf90c3c8430de5a66de6e503de1e047ae3397a4", + "bindings.ledIntensityHelp": "644d876e9a276e7ff74d0d4eeee5351ab05a462f2325f8e7d4ae3697111e6659", "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", @@ -5566,9 +5566,9 @@ "bindings.ledMode": "8b797e51f1310507dcdfef4cf8dc8db164afe34a3ad5595e454bb0b24e6c18cb", "bindings.ledModeHelp": "e1fdb51d3321ded27e44b28d52acea95e94aa578e23fcd3dde0e8590322e94b1", "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", - "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", - "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.ledMusicReactive": "4187beb90a5e4423eb405f60ad6849231d34f76f1c8bdcd4eef222460282c18a", + "bindings.ledIntensity": "37aede6ebb44595af3d3f6a85cf90c3c8430de5a66de6e503de1e047ae3397a4", + "bindings.ledIntensityHelp": "644d876e9a276e7ff74d0d4eeee5351ab05a462f2325f8e7d4ae3697111e6659", "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", @@ -6263,9 +6263,9 @@ "bindings.ledMode": "8b797e51f1310507dcdfef4cf8dc8db164afe34a3ad5595e454bb0b24e6c18cb", "bindings.ledModeHelp": "e1fdb51d3321ded27e44b28d52acea95e94aa578e23fcd3dde0e8590322e94b1", "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", - "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", - "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.ledMusicReactive": "4187beb90a5e4423eb405f60ad6849231d34f76f1c8bdcd4eef222460282c18a", + "bindings.ledIntensity": "37aede6ebb44595af3d3f6a85cf90c3c8430de5a66de6e503de1e047ae3397a4", + "bindings.ledIntensityHelp": "644d876e9a276e7ff74d0d4eeee5351ab05a462f2325f8e7d4ae3697111e6659", "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", @@ -6960,9 +6960,9 @@ "bindings.ledMode": "8b797e51f1310507dcdfef4cf8dc8db164afe34a3ad5595e454bb0b24e6c18cb", "bindings.ledModeHelp": "e1fdb51d3321ded27e44b28d52acea95e94aa578e23fcd3dde0e8590322e94b1", "bindings.ledFollowValue": "c03c266e971f4a504e7dd0aa4166d73ea6c91fce08d035815b7e63b43747f32f", - "bindings.ledMusicReactive": "b65d5988ffa274aafe9411bfd2fa47e5e2437019729c9b64a56293648423a19a", - "bindings.ledIntensity": "aa53d5a8931523221e560531cb9fb874ed80c0fe812dcbe151784c669fc50052", - "bindings.ledIntensityHelp": "e79ded5a0c73e44249bc54203bd1b9bd7a8583377c2089bff61006630c0fc4da", + "bindings.ledMusicReactive": "4187beb90a5e4423eb405f60ad6849231d34f76f1c8bdcd4eef222460282c18a", + "bindings.ledIntensity": "37aede6ebb44595af3d3f6a85cf90c3c8430de5a66de6e503de1e047ae3397a4", + "bindings.ledIntensityHelp": "644d876e9a276e7ff74d0d4eeee5351ab05a462f2325f8e7d4ae3697111e6659", "bindings.testLed": "80ccc51db9708fb9e6e053877a81fca717ca0348be079cf746f8446a878bd083", "bindings.testLedHelp": "76129f95c4917ccc81da4a9cafa5523856933aadff158bbfd4c8023ebd4d1c66", "bindings.testLedTesting": "f411c8dfb627c9040c05ca2800031645dfdd82bb1dab0c7b46d950781f760972", @@ -7010,5 +7010,10 @@ "settings.appearance.instantApply": "Changes are applied instantly.", "settings.appearance.restartNote": "Some settings may require an application restart to take full effect.", "settings.appearance.exportTheme": "Export Theme", - "settings.appearance.importTheme": "Import Theme" + "settings.appearance.importTheme": "Import Theme", + "en": { + "bindings.ledMusicReactive": "4187beb90a5e4423eb405f60ad6849231d34f76f1c8bdcd4eef222460282c18a", + "bindings.ledIntensity": "37aede6ebb44595af3d3f6a85cf90c3c8430de5a66de6e503de1e047ae3397a4", + "bindings.ledIntensityHelp": "644d876e9a276e7ff74d0d4eeee5351ab05a462f2325f8e7d4ae3697111e6659" + } } diff --git a/src/locales/de.json b/src/locales/de.json index 97e710f..61db420 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -688,9 +688,9 @@ "bindings.ledMode": "LED mode", "bindings.ledModeHelp": "Follow the fader value, or pulse the light with the bound audio level.", "bindings.ledFollowValue": "Follow Value", - "bindings.ledMusicReactive": "Music Reactive", - "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.ledMusicReactive": "Audio Reactive", + "bindings.ledIntensity": "LED sensitivity", + "bindings.ledIntensityHelp": "How strongly the light reacts to peaks and small level changes around the current meter.", "bindings.testLed": "Test LED", "bindings.testLedHelp": "Flash the configured LED address three times.", "bindings.testLedTesting": "Flashing LED…", diff --git a/src/locales/en.json b/src/locales/en.json index be7f61d..a2efce0 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -162,9 +162,9 @@ "bindings.ledMode": "LED mode", "bindings.ledModeHelp": "Follow the fader value, or pulse the light with the bound audio level.", "bindings.ledFollowValue": "Follow Value", - "bindings.ledMusicReactive": "Music Reactive", - "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.ledMusicReactive": "Audio Reactive", + "bindings.ledIntensity": "LED sensitivity", + "bindings.ledIntensityHelp": "How strongly the light reacts to peaks and small level changes around the current meter.", "bindings.pitchBend": "Pitch Bend", "bindings.learnFeedbackOutput": "Learn feedback output", "bindings.resetFeedbackOutput": "Reset feedback output", diff --git a/src/locales/es.json b/src/locales/es.json index 29003dd..f75eeb2 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -688,9 +688,9 @@ "bindings.ledMode": "LED mode", "bindings.ledModeHelp": "Follow the fader value, or pulse the light with the bound audio level.", "bindings.ledFollowValue": "Follow Value", - "bindings.ledMusicReactive": "Music Reactive", - "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.ledMusicReactive": "Audio Reactive", + "bindings.ledIntensity": "LED sensitivity", + "bindings.ledIntensityHelp": "How strongly the light reacts to peaks and small level changes around the current meter.", "bindings.testLed": "Test LED", "bindings.testLedHelp": "Flash the configured LED address three times.", "bindings.testLedTesting": "Flashing LED…", diff --git a/src/locales/fr.json b/src/locales/fr.json index 2173f9a..b2efa32 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -688,9 +688,9 @@ "bindings.ledMode": "LED mode", "bindings.ledModeHelp": "Follow the fader value, or pulse the light with the bound audio level.", "bindings.ledFollowValue": "Follow Value", - "bindings.ledMusicReactive": "Music Reactive", - "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.ledMusicReactive": "Audio Reactive", + "bindings.ledIntensity": "LED sensitivity", + "bindings.ledIntensityHelp": "How strongly the light reacts to peaks and small level changes around the current meter.", "bindings.testLed": "Test LED", "bindings.testLedHelp": "Flash the configured LED address three times.", "bindings.testLedTesting": "Flashing LED…", diff --git a/src/locales/it.json b/src/locales/it.json index 4615eb6..fa1b8a1 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -688,9 +688,9 @@ "bindings.ledMode": "LED mode", "bindings.ledModeHelp": "Follow the fader value, or pulse the light with the bound audio level.", "bindings.ledFollowValue": "Follow Value", - "bindings.ledMusicReactive": "Music Reactive", - "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.ledMusicReactive": "Audio Reactive", + "bindings.ledIntensity": "LED sensitivity", + "bindings.ledIntensityHelp": "How strongly the light reacts to peaks and small level changes around the current meter.", "bindings.testLed": "Test LED", "bindings.testLedHelp": "Flash the configured LED address three times.", "bindings.testLedTesting": "Flashing LED…", diff --git a/src/locales/ja.json b/src/locales/ja.json index ff551c5..b02f9d2 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -688,9 +688,9 @@ "bindings.ledMode": "LED mode", "bindings.ledModeHelp": "Follow the fader value, or pulse the light with the bound audio level.", "bindings.ledFollowValue": "Follow Value", - "bindings.ledMusicReactive": "Music Reactive", - "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.ledMusicReactive": "Audio Reactive", + "bindings.ledIntensity": "LED sensitivity", + "bindings.ledIntensityHelp": "How strongly the light reacts to peaks and small level changes around the current meter.", "bindings.testLed": "Test LED", "bindings.testLedHelp": "Flash the configured LED address three times.", "bindings.testLedTesting": "Flashing LED…", diff --git a/src/locales/ko.json b/src/locales/ko.json index 4b9891b..98a8719 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -688,9 +688,9 @@ "bindings.ledMode": "LED mode", "bindings.ledModeHelp": "Follow the fader value, or pulse the light with the bound audio level.", "bindings.ledFollowValue": "Follow Value", - "bindings.ledMusicReactive": "Music Reactive", - "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.ledMusicReactive": "Audio Reactive", + "bindings.ledIntensity": "LED sensitivity", + "bindings.ledIntensityHelp": "How strongly the light reacts to peaks and small level changes around the current meter.", "bindings.testLed": "Test LED", "bindings.testLedHelp": "Flash the configured LED address three times.", "bindings.testLedTesting": "Flashing LED…", diff --git a/src/locales/nl.json b/src/locales/nl.json index 1c4917a..82d586e 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -688,9 +688,9 @@ "bindings.ledMode": "LED mode", "bindings.ledModeHelp": "Follow the fader value, or pulse the light with the bound audio level.", "bindings.ledFollowValue": "Follow Value", - "bindings.ledMusicReactive": "Music Reactive", - "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.ledMusicReactive": "Audio Reactive", + "bindings.ledIntensity": "LED sensitivity", + "bindings.ledIntensityHelp": "How strongly the light reacts to peaks and small level changes around the current meter.", "bindings.testLed": "Test LED", "bindings.testLedHelp": "Flash the configured LED address three times.", "bindings.testLedTesting": "Flashing LED…", diff --git a/src/locales/pl.json b/src/locales/pl.json index 7484fb0..04a588e 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -688,9 +688,9 @@ "bindings.ledMode": "LED mode", "bindings.ledModeHelp": "Follow the fader value, or pulse the light with the bound audio level.", "bindings.ledFollowValue": "Follow Value", - "bindings.ledMusicReactive": "Music Reactive", - "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.ledMusicReactive": "Audio Reactive", + "bindings.ledIntensity": "LED sensitivity", + "bindings.ledIntensityHelp": "How strongly the light reacts to peaks and small level changes around the current meter.", "bindings.testLed": "Test LED", "bindings.testLedHelp": "Flash the configured LED address three times.", "bindings.testLedTesting": "Flashing LED…", diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json index a382c08..19e56b2 100644 --- a/src/locales/pt-BR.json +++ b/src/locales/pt-BR.json @@ -688,9 +688,9 @@ "bindings.ledMode": "LED mode", "bindings.ledModeHelp": "Follow the fader value, or pulse the light with the bound audio level.", "bindings.ledFollowValue": "Follow Value", - "bindings.ledMusicReactive": "Music Reactive", - "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.ledMusicReactive": "Audio Reactive", + "bindings.ledIntensity": "LED sensitivity", + "bindings.ledIntensityHelp": "How strongly the light reacts to peaks and small level changes around the current meter.", "bindings.testLed": "Test LED", "bindings.testLedHelp": "Flash the configured LED address three times.", "bindings.testLedTesting": "Flashing LED…", diff --git a/src/locales/zh-Hans.json b/src/locales/zh-Hans.json index 32f9d0f..f570509 100644 --- a/src/locales/zh-Hans.json +++ b/src/locales/zh-Hans.json @@ -688,9 +688,9 @@ "bindings.ledMode": "LED mode", "bindings.ledModeHelp": "Follow the fader value, or pulse the light with the bound audio level.", "bindings.ledFollowValue": "Follow Value", - "bindings.ledMusicReactive": "Music Reactive", - "bindings.ledIntensity": "LED intensity", - "bindings.ledIntensityHelp": "How strongly the light reacts to audio peaks.", + "bindings.ledMusicReactive": "Audio Reactive", + "bindings.ledIntensity": "LED sensitivity", + "bindings.ledIntensityHelp": "How strongly the light reacts to peaks and small level changes around the current meter.", "bindings.testLed": "Test LED", "bindings.testLedHelp": "Flash the configured LED address three times.", "bindings.testLedTesting": "Flashing LED…",