Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.).

### Audio-reactive LEDs

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 **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 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

### 1) Configure MIDI Routes
Expand Down
1 change: 1 addition & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ windows = { version = "0.61", features = [
"Win32_System_Diagnostics_ToolHelp",
"Win32_System_Threading",
"Win32_System_Registry",
"Win32_System_Variant",
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_Storage_FileSystem",
Expand Down
21 changes: 21 additions & 0 deletions src-tauri/src/audio/mod.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub process_name: Option<String>,
pub process_path: Option<String>,
}

#[derive(Debug, Clone, Default)]
pub struct AudioPeakLevels {
pub master: f32,
pub devices: HashMap<String, f32>,
pub sessions: HashMap<String, f32>,
pub focused_session_id: Option<String>,
pub session_matches: Vec<(SessionPeakIdentity, f32)>,
}

pub trait AudioBackend: Send + Sync {
fn list_sessions(&self) -> anyhow::Result<Vec<SessionInfo>>;
Expand All @@ -16,6 +34,9 @@ pub trait AudioBackend: Send + Sync {
fn focused_session_state(&self) -> anyhow::Result<Option<SessionInfo>> {
self.focused_session()
}
fn peak_levels(&self) -> anyhow::Result<AudioPeakLevels> {
Ok(AudioPeakLevels::default())
}

// Mute methods
fn set_master_mute(&self, muted: bool) -> anyhow::Result<()>;
Expand Down
145 changes: 143 additions & 2 deletions src-tauri/src/audio/windows.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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,
Expand Down Expand Up @@ -208,6 +208,10 @@ impl AudioBackend for WindowsAudioBackend {
list_sessions_with_visuals(false)
}

fn peak_levels(&self) -> Result<AudioPeakLevels> {
collect_peak_levels()
}

fn list_playback_devices(&self) -> Result<Vec<PlaybackDeviceInfo>> {
let _com = init_com()?;
let enumerator = get_device_enumerator()?;
Expand Down Expand Up @@ -1078,6 +1082,143 @@ fn get_endpoint_volume(
Ok(endpoint)
}

fn endpoint_peak_value(device: &IMMDevice) -> f32 {
let meter: IAudioMeterInformation = match unsafe { device.Activate(CLSCTX_ALL, None) } {
Ok(meter) => meter,
Err(_) => return 0.0,
};
match unsafe { meter.GetPeakValue() } {
Ok(value) => value.clamp(0.0, 1.0),
Err(_) => 0.0,
}
}

fn session_peak_value(control: &IAudioSessionControl2, simple: &ISimpleAudioVolume) -> f32 {
let muted = unsafe { simple.GetMute() }
.map(|value| value.as_bool())
.unwrap_or(false);
if muted {
return 0.0;
}
let meter: IAudioMeterInformation = match control.cast() {
Ok(meter) => meter,
Err(_) => return 0.0,
};
match unsafe { meter.GetPeakValue() } {
Ok(value) => value.clamp(0.0, 1.0),
Err(_) => 0.0,
}
}

fn collect_peak_levels() -> Result<AudioPeakLevels> {
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<String> {
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<IAudioSessionManager2> {
Expand Down
Loading