diff --git a/components/flow3r_bsp/flow3r_bsp.h b/components/flow3r_bsp/flow3r_bsp.h
index 4cd89a290e99cdca915444a27c9851683db28713..4505a55891da2f245c43cb2bf2448d847bac99ed 100644
--- a/components/flow3r_bsp/flow3r_bsp.h
+++ b/components/flow3r_bsp/flow3r_bsp.h
@@ -51,7 +51,10 @@ typedef enum {
     // Headset microphone on left jack.
     flow3r_bsp_audio_input_source_headset_mic = 2,
     // Onboard microphone (enabled red LED).
-    flow3r_bsp_audio_input_source_onboard_mic = 3
+    flow3r_bsp_audio_input_source_onboard_mic = 3,
+    // auto switching depending on availability
+    // line in preferred to headset mic preferred to onboard mic.
+    flow3r_bsp_audio_input_source_auto = 4
 } flow3r_bsp_audio_input_source_t;
 
 // Initialize the audio subsystem of the badge, including the codec and I2S data
diff --git a/components/flow3r_bsp/flow3r_bsp_max98091.c b/components/flow3r_bsp/flow3r_bsp_max98091.c
index 977c0279643950eee091a2a950c597534d9e4f85..3cde01e4437a54ac6a043246167284e03a03da46 100644
--- a/components/flow3r_bsp/flow3r_bsp_max98091.c
+++ b/components/flow3r_bsp/flow3r_bsp_max98091.c
@@ -262,6 +262,8 @@ void flow3r_bsp_max98091_input_set_source(
             max98091_check(MAX98091_LEFT_ADC_MIXER, 0);
             max98091_check(MAX98091_RIGHT_ADC_MIXER, 0);
             break;
+        case flow3r_bsp_audio_input_source_auto:
+            break;
     }
 }
 
diff --git a/components/micropython/usermodule/mp_audio.c b/components/micropython/usermodule/mp_audio.c
index e511d5a62d09531bdffdf5a3153e6d24bfd93ebd..a0f5cdb56126ba97049e34b9fe1cdb9f9c60cd2f 100644
--- a/components/micropython/usermodule/mp_audio.c
+++ b/components/micropython/usermodule/mp_audio.c
@@ -195,22 +195,22 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_volume_relative_obj,
                                  mp_get_volume_relative);
 
 STATIC mp_obj_t mp_headphones_line_in_set_hardware_thru(mp_obj_t enable) {
-    st3m_audio_headphones_line_in_set_hardware_thru(mp_obj_get_int(enable));
     return mp_const_none;
+    // st3m_audio_headphones_line_in_set_hardware_thru(mp_obj_get_int(enable));
 }
 STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_headphones_line_in_set_hardware_thru_obj,
                                  mp_headphones_line_in_set_hardware_thru);
 
 STATIC mp_obj_t mp_speaker_line_in_set_hardware_thru(mp_obj_t enable) {
-    st3m_audio_speaker_line_in_set_hardware_thru(mp_obj_get_int(enable));
     return mp_const_none;
+    // st3m_audio_speaker_line_in_set_hardware_thru(mp_obj_get_int(enable));
 }
 STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_speaker_line_in_set_hardware_thru_obj,
                                  mp_speaker_line_in_set_hardware_thru);
 
 STATIC mp_obj_t mp_line_in_set_hardware_thru(mp_obj_t enable) {
-    st3m_audio_line_in_set_hardware_thru(mp_obj_get_int(enable));
     return mp_const_none;
+    // st3m_audio_line_in_set_hardware_thru(mp_obj_get_int(enable));
 }
 STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_line_in_set_hardware_thru_obj,
                                  mp_line_in_set_hardware_thru);
@@ -219,29 +219,111 @@ STATIC mp_obj_t mp_line_in_is_connected() {
 }
 STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_line_in_is_connected_obj,
                                  mp_line_in_is_connected);
-STATIC mp_obj_t mp_input_set_source(mp_obj_t source) {
-    st3m_audio_input_set_source(mp_obj_get_int(source));
+
+// <INPUT SETUP>
+
+// engine
+STATIC mp_obj_t mp_input_engine_set_source(mp_obj_t source) {
+    st3m_audio_input_engine_set_source(mp_obj_get_int(source));
     return mp_const_none;
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_input_set_source_obj, mp_input_set_source);
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_input_engine_set_source_obj,
+                                 mp_input_engine_set_source);
+
+STATIC mp_obj_t mp_input_engine_get_source(void) {
+    return mp_obj_new_int(st3m_audio_input_engine_get_source());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_input_engine_get_source_obj,
+                                 mp_input_engine_get_source);
+
+STATIC mp_obj_t mp_input_engine_get_target_source(void) {
+    return mp_obj_new_int(st3m_audio_input_engine_get_target_source());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_input_engine_get_target_source_obj,
+                                 mp_input_engine_get_target_source);
+
+STATIC mp_obj_t mp_input_engine_get_source_avail(mp_obj_t source) {
+    return mp_obj_new_int(
+        st3m_audio_input_engine_get_source_avail(mp_obj_get_int(source)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_input_engine_get_source_avail_obj,
+                                 mp_input_engine_get_source_avail);
 
+// thru
+STATIC mp_obj_t mp_input_thru_set_source(mp_obj_t source) {
+    st3m_audio_input_thru_set_source(mp_obj_get_int(source));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_input_thru_set_source_obj,
+                                 mp_input_thru_set_source);
+
+STATIC mp_obj_t mp_input_thru_get_source(void) {
+    return mp_obj_new_int(st3m_audio_input_thru_get_source());
+}
+
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_input_thru_get_source_obj,
+                                 mp_input_thru_get_source);
+
+STATIC mp_obj_t mp_input_thru_get_target_source(void) {
+    return mp_obj_new_int(st3m_audio_input_thru_get_target_source());
+}
+
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_input_thru_get_target_source_obj,
+                                 mp_input_thru_get_target_source);
+
+STATIC mp_obj_t mp_input_thru_get_source_avail(mp_obj_t source) {
+    return mp_obj_new_int(
+        st3m_audio_input_thru_get_source_avail(mp_obj_get_int(source)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_input_thru_get_source_avail_obj,
+                                 mp_input_thru_get_source_avail);
+
+// actual source
 STATIC mp_obj_t mp_input_get_source(void) {
     return mp_obj_new_int(st3m_audio_input_get_source());
 }
 STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_input_get_source_obj, mp_input_get_source);
 
-STATIC mp_obj_t mp_headset_set_gain_dB(mp_obj_t gain_dB) {
-    st3m_audio_headset_set_gain_dB(mp_obj_get_int(gain_dB));
-    return mp_const_none;
+// gain
+STATIC mp_obj_t mp_headset_mic_set_gain_dB(mp_obj_t gain_dB) {
+    float ret = st3m_audio_headset_mic_set_gain_dB(mp_obj_get_float(gain_dB));
+    return mp_obj_new_float(ret);
+}
+
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_headset_mic_set_gain_dB_obj,
+                                 mp_headset_mic_set_gain_dB);
+
+STATIC mp_obj_t mp_headset_mic_get_gain_dB(void) {
+    return mp_obj_new_float(st3m_audio_headset_mic_get_gain_dB());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_headset_mic_get_gain_dB_obj,
+                                 mp_headset_mic_get_gain_dB);
+
+STATIC mp_obj_t mp_onboard_mic_set_gain_dB(mp_obj_t gain_dB) {
+    float ret = st3m_audio_onboard_mic_set_gain_dB(mp_obj_get_float(gain_dB));
+    return mp_obj_new_float(ret);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_onboard_mic_set_gain_dB_obj,
+                                 mp_onboard_mic_set_gain_dB);
+
+STATIC mp_obj_t mp_onboard_mic_get_gain_dB(void) {
+    return mp_obj_new_float(st3m_audio_onboard_mic_get_gain_dB());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_onboard_mic_get_gain_dB_obj,
+                                 mp_onboard_mic_get_gain_dB);
+
+STATIC mp_obj_t mp_line_in_set_gain_dB(mp_obj_t gain_dB) {
+    float ret = st3m_audio_line_in_set_gain_dB(mp_obj_get_float(gain_dB));
+    return mp_obj_new_float(ret);
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_headset_set_gain_dB_obj,
-                                 mp_headset_set_gain_dB);
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_line_in_set_gain_dB_obj,
+                                 mp_line_in_set_gain_dB);
 
-STATIC mp_obj_t mp_headset_get_gain_dB(void) {
-    return mp_obj_new_int(st3m_audio_headset_get_gain_dB());
+STATIC mp_obj_t mp_line_in_get_gain_dB(void) {
+    return mp_obj_new_float(st3m_audio_line_in_get_gain_dB());
 }
-STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_headset_get_gain_dB_obj,
-                                 mp_headset_get_gain_dB);
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_line_in_get_gain_dB_obj,
+                                 mp_line_in_get_gain_dB);
 
 STATIC mp_obj_t mp_input_thru_set_volume_dB(mp_obj_t vol_dB) {
     return mp_obj_new_float(
@@ -269,6 +351,8 @@ STATIC mp_obj_t mp_input_thru_get_mute() {
 STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_input_thru_get_mute_obj,
                                  mp_input_thru_get_mute);
 
+// eq
+
 STATIC mp_obj_t mp_speaker_get_eq_on() {
     return mp_obj_new_int(st3m_audio_speaker_get_eq_on());
 }
@@ -282,6 +366,61 @@ STATIC mp_obj_t mp_speaker_set_eq_on(mp_obj_t eq_on) {
 STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_speaker_set_eq_on_obj,
                                  mp_speaker_set_eq_on);
 
+// permissions
+
+STATIC mp_obj_t mp_headset_mic_set_allowed(mp_obj_t allowed) {
+    st3m_audio_headset_mic_set_allowed(mp_obj_get_int(allowed));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_headset_mic_set_allowed_obj,
+                                 mp_headset_mic_set_allowed);
+
+STATIC mp_obj_t mp_headset_mic_get_allowed() {
+    return mp_obj_new_int(st3m_audio_headset_mic_get_allowed());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_headset_mic_get_allowed_obj,
+                                 mp_headset_mic_get_allowed);
+
+STATIC mp_obj_t mp_onboard_mic_set_allowed(mp_obj_t allowed) {
+    st3m_audio_onboard_mic_set_allowed(mp_obj_get_int(allowed));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_onboard_mic_set_allowed_obj,
+                                 mp_onboard_mic_set_allowed);
+
+STATIC mp_obj_t mp_onboard_mic_get_allowed() {
+    return mp_obj_new_int(st3m_audio_onboard_mic_get_allowed());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_onboard_mic_get_allowed_obj,
+                                 mp_onboard_mic_get_allowed);
+
+STATIC mp_obj_t mp_line_in_set_allowed(mp_obj_t allowed) {
+    st3m_audio_line_in_set_allowed(mp_obj_get_int(allowed));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_line_in_set_allowed_obj,
+                                 mp_line_in_set_allowed);
+
+STATIC mp_obj_t mp_line_in_get_allowed() {
+    return mp_obj_new_int(st3m_audio_line_in_get_allowed());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_line_in_get_allowed_obj,
+                                 mp_line_in_get_allowed);
+
+STATIC mp_obj_t mp_onboard_mic_to_speaker_set_allowed(mp_obj_t allowed) {
+    st3m_audio_onboard_mic_to_speaker_set_allowed(mp_obj_get_int(allowed));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_onboard_mic_to_speaker_set_allowed_obj,
+                                 mp_onboard_mic_to_speaker_set_allowed);
+
+STATIC mp_obj_t mp_onboard_mic_to_speaker_get_allowed() {
+    return mp_obj_new_int(st3m_audio_onboard_mic_to_speaker_get_allowed());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_onboard_mic_to_speaker_get_allowed_obj,
+                                 mp_onboard_mic_to_speaker_get_allowed);
+// </INPUT SETUP>
+
 STATIC mp_obj_t mp_codec_i2c_write(mp_obj_t reg_in, mp_obj_t data_in) {
 #if defined(CONFIG_FLOW3R_HW_GEN_P3) || defined(CONFIG_FLOW3R_HW_GEN_P4) || \
     defined(CONFIG_FLOW3R_HW_GEN_C23)
@@ -372,15 +511,44 @@ STATIC const mp_rom_map_elem_t mp_module_audio_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR_line_in_is_connected),
       MP_ROM_PTR(&mp_line_in_is_connected_obj) },
 
-    { MP_ROM_QSTR(MP_QSTR_input_set_source),
-      MP_ROM_PTR(&mp_input_set_source_obj) },
+    { MP_ROM_QSTR(MP_QSTR_input_engine_set_source),
+      MP_ROM_PTR(&mp_input_engine_set_source_obj) },
+    { MP_ROM_QSTR(MP_QSTR_input_engine_get_source),
+      MP_ROM_PTR(&mp_input_engine_get_source_obj) },
+    { MP_ROM_QSTR(MP_QSTR_input_engine_get_target_source),
+      MP_ROM_PTR(&mp_input_engine_get_target_source_obj) },
+    { MP_ROM_QSTR(MP_QSTR_input_engine_get_source_avail),
+      MP_ROM_PTR(&mp_input_engine_get_source_avail_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_input_thru_set_source),
+      MP_ROM_PTR(&mp_input_thru_set_source_obj) },
+    { MP_ROM_QSTR(MP_QSTR_input_thru_get_source),
+      MP_ROM_PTR(&mp_input_thru_get_source_obj) },
+    { MP_ROM_QSTR(MP_QSTR_input_thru_get_target_source),
+      MP_ROM_PTR(&mp_input_thru_get_target_source_obj) },
+    { MP_ROM_QSTR(MP_QSTR_input_thru_get_source_avail),
+      MP_ROM_PTR(&mp_input_thru_get_source_avail_obj) },
+
     { MP_ROM_QSTR(MP_QSTR_input_get_source),
       MP_ROM_PTR(&mp_input_get_source_obj) },
+    // TODO: DEPRECATE
+    { MP_ROM_QSTR(MP_QSTR_input_set_source),
+      MP_ROM_PTR(&mp_input_engine_set_source_obj) },
 
-    { MP_ROM_QSTR(MP_QSTR_headset_set_gain_dB),
-      MP_ROM_PTR(&mp_headset_set_gain_dB_obj) },
-    { MP_ROM_QSTR(MP_QSTR_headset_get_gain_dB),
-      MP_ROM_PTR(&mp_headset_get_gain_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_headset_mic_set_gain_dB),
+      MP_ROM_PTR(&mp_headset_mic_set_gain_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_headset_mic_get_gain_dB),
+      MP_ROM_PTR(&mp_headset_mic_get_gain_dB_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_onboard_mic_set_gain_dB),
+      MP_ROM_PTR(&mp_onboard_mic_set_gain_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_onboard_mic_get_gain_dB),
+      MP_ROM_PTR(&mp_onboard_mic_get_gain_dB_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_line_in_set_gain_dB),
+      MP_ROM_PTR(&mp_line_in_set_gain_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_line_in_get_gain_dB),
+      MP_ROM_PTR(&mp_line_in_get_gain_dB_obj) },
 
     { MP_ROM_QSTR(MP_QSTR_input_thru_set_volume_dB),
       MP_ROM_PTR(&mp_input_thru_set_volume_dB_obj) },
@@ -402,11 +570,30 @@ STATIC const mp_rom_map_elem_t mp_module_audio_globals_table[] = {
       MP_ROM_INT(st3m_audio_input_source_headset_mic) },
     { MP_ROM_QSTR(MP_QSTR_INPUT_SOURCE_ONBOARD_MIC),
       MP_ROM_INT(st3m_audio_input_source_onboard_mic) },
+    { MP_ROM_QSTR(MP_QSTR_INPUT_SOURCE_AUTO),
+      MP_ROM_INT(st3m_audio_input_source_auto) },
 
     { MP_ROM_QSTR(MP_QSTR_speaker_get_eq_on),
       MP_ROM_PTR(&mp_speaker_get_eq_on_obj) },
     { MP_ROM_QSTR(MP_QSTR_speaker_set_eq_on),
       MP_ROM_PTR(&mp_speaker_set_eq_on_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_headset_mic_get_allowed),
+      MP_ROM_PTR(&mp_headset_mic_get_allowed_obj) },
+    { MP_ROM_QSTR(MP_QSTR_headset_mic_set_allowed),
+      MP_ROM_PTR(&mp_headset_mic_set_allowed_obj) },
+    { MP_ROM_QSTR(MP_QSTR_onboard_mic_get_allowed),
+      MP_ROM_PTR(&mp_onboard_mic_get_allowed_obj) },
+    { MP_ROM_QSTR(MP_QSTR_onboard_mic_set_allowed),
+      MP_ROM_PTR(&mp_onboard_mic_set_allowed_obj) },
+    { MP_ROM_QSTR(MP_QSTR_line_in_get_allowed),
+      MP_ROM_PTR(&mp_line_in_get_allowed_obj) },
+    { MP_ROM_QSTR(MP_QSTR_line_in_set_allowed),
+      MP_ROM_PTR(&mp_line_in_set_allowed_obj) },
+    { MP_ROM_QSTR(MP_QSTR_onboard_mic_to_speaker_get_allowed),
+      MP_ROM_PTR(&mp_onboard_mic_to_speaker_get_allowed_obj) },
+    { MP_ROM_QSTR(MP_QSTR_onboard_mic_to_speaker_set_allowed),
+      MP_ROM_PTR(&mp_onboard_mic_to_speaker_set_allowed_obj) },
 };
 
 STATIC MP_DEFINE_CONST_DICT(mp_module_audio_globals,
diff --git a/components/st3m/st3m_audio.c b/components/st3m/st3m_audio.c
index cb7324d0dfaf7ed1a10b9893c003a851250cdeb2..78b10d24d30eea8e5eae25ce67e393f392ae3cc1 100644
--- a/components/st3m/st3m_audio.c
+++ b/components/st3m/st3m_audio.c
@@ -31,7 +31,7 @@ static bool _headphones_connected(void);
 const static float headphones_maximum_volume_system_dB = 3;
 const static float speaker_maximum_volume_system_dB = 14;
 
-// Output, either speakers or headphones. Holds volume/mute state and limits,
+// Output, either speaker or headphones. Holds volume/mute state and limits,
 // and calculated software volume.
 //
 // An output's apply function configures the actual physical output, ie. by
@@ -184,12 +184,25 @@ typedef struct {
     st3m_audio_output_t headphones;
     st3m_audio_output_t speaker;
 
-    // Denormalized setting data that can be read back by user.
-    st3m_audio_input_source_t source;
-    int8_t headset_gain;
+    float headset_mic_gain_dB;
+    int16_t headset_mic_gain_software;
+    float onboard_mic_gain_dB;
+    int16_t onboard_mic_gain_software;
+    float line_in_gain_dB;
+    int16_t line_in_gain_software;
 
     uint8_t speaker_eq_on;
 
+    bool headset_mic_allowed;
+    bool onboard_mic_allowed;
+    bool line_in_allowed;
+    bool onboard_mic_to_speaker_allowed;
+    st3m_audio_input_source_t engine_source;
+    st3m_audio_input_source_t engine_target_source;
+    st3m_audio_input_source_t thru_source;
+    st3m_audio_input_source_t thru_target_source;
+    st3m_audio_input_source_t source;
+
     // Software-based audio pipe settings.
     int32_t input_thru_vol;
     int32_t input_thru_vol_int;
@@ -230,11 +243,28 @@ static st3m_audio_state_t state = {
             .apply = _audio_speaker_apply,
         },
 
-    .source = st3m_audio_input_source_none,
-    .headset_gain = 0,
+    .speaker_eq_on = true,
+
+    .headset_mic_allowed = true,
+    .onboard_mic_allowed = true,
+    .line_in_allowed = true,
+    .onboard_mic_to_speaker_allowed = false,
+    .headset_mic_gain_dB = 0,
+    .onboard_mic_gain_dB = 0,
+    .line_in_gain_dB = 0,
+    .headset_mic_gain_software = 256,
+    .onboard_mic_gain_software = 256,
+    .line_in_gain_software = 256,
+
     .input_thru_vol = 0,
-    .input_thru_vol_int = 0,
-    .input_thru_mute = true,
+    .input_thru_vol_int = 32768,
+    .input_thru_mute = false, // deprecated
+
+    .engine_target_source = st3m_audio_input_source_none,
+    .engine_source = st3m_audio_input_source_none,
+    .thru_source = st3m_audio_input_source_none,
+    .thru_target_source = st3m_audio_input_source_none,
+    .source = st3m_audio_input_source_none,
     .function = st3m_audio_player_function_dummy,
 };
 
@@ -246,11 +276,243 @@ static bool _headphones_connected(void) {
     return state.jacksense.headphones || state.headphones_detection_override;
 }
 
-static void _update_jacksense() {
+static void _audio_input_set_source(st3m_audio_input_source_t source) {
+    LOCK;
+    st3m_audio_input_source_t prev_source = state.source;
+    UNLOCK;
+    if (source == prev_source) return;
+
+    flow3r_bsp_audio_input_source_t fsource;
+    switch (source) {
+        case st3m_audio_input_source_line_in:
+            fsource = flow3r_bsp_audio_input_source_line_in;
+            break;
+        case st3m_audio_input_source_onboard_mic:
+            fsource = flow3r_bsp_audio_input_source_onboard_mic;
+            break;
+        case st3m_audio_input_source_headset_mic:
+            fsource = flow3r_bsp_audio_input_source_headset_mic;
+            break;
+        case st3m_audio_input_source_none:
+            fsource = flow3r_bsp_audio_input_source_none;
+            break;
+        case st3m_audio_input_source_auto:
+            fsource = flow3r_bsp_audio_input_source_none;
+            break;
+    }
+    LOCK;
+    state.source = source;
+    UNLOCK;
+    flow3r_bsp_audio_input_set_source(fsource);
+}
+
+static bool _check_engine_source_avail(st3m_audio_input_source_t source) {
+    switch (source) {
+        case st3m_audio_input_source_none:
+            return true;
+        case st3m_audio_input_source_auto:
+            return true;
+        case st3m_audio_input_source_line_in:
+            return state.line_in_allowed && state.jacksense.line_in;
+        case st3m_audio_input_source_headset_mic:
+            return state.headset_mic_allowed && state.jacksense.headset;
+        case st3m_audio_input_source_onboard_mic:
+            return state.onboard_mic_allowed;
+    }
+    return false;
+}
+
+bool st3m_audio_input_engine_get_source_avail(
+    st3m_audio_input_source_t source) {
+    bool ret = false;
+    if (source == st3m_audio_input_source_auto) {
+        LOCK;
+        ret =
+            ret || _check_engine_source_avail(st3m_audio_input_source_line_in);
+        ret = ret ||
+              _check_engine_source_avail(st3m_audio_input_source_headset_mic);
+        ret = ret ||
+              _check_engine_source_avail(st3m_audio_input_source_onboard_mic);
+        UNLOCK;
+    } else {
+        LOCK;
+        ret = _check_engine_source_avail(source);
+        UNLOCK;
+    }
+    return ret;
+}
+
+void _update_engine_source() {
+    st3m_audio_input_source_t source;
+    LOCK;
+    source = state.engine_target_source;
+    if (source == st3m_audio_input_source_auto) {
+        if (_check_engine_source_avail(st3m_audio_input_source_line_in)) {
+            source = st3m_audio_input_source_line_in;
+        } else if (_check_engine_source_avail(
+                       st3m_audio_input_source_headset_mic)) {
+            source = st3m_audio_input_source_headset_mic;
+        } else if (_check_engine_source_avail(
+                       st3m_audio_input_source_onboard_mic)) {
+            source = st3m_audio_input_source_onboard_mic;
+        } else {
+            source = st3m_audio_input_source_none;
+        }
+    }
+    bool avail = _check_engine_source_avail(source);
+    UNLOCK;
+    source = avail ? source : st3m_audio_input_source_none;
+    LOCK;
+    bool return_early = state.source == source;
+    state.engine_source = source;
+    UNLOCK;
+    if (return_early) return;
+
+    if (avail) {
+        switch (source) {
+            case st3m_audio_input_source_none:
+                break;
+            case st3m_audio_input_source_auto:
+                break;
+            case st3m_audio_input_source_line_in:
+                if (avail) {
+                    _audio_input_set_source(st3m_audio_input_source_line_in);
+                }
+                break;
+            case st3m_audio_input_source_headset_mic:
+                if (avail) {
+                    _audio_input_set_source(
+                        st3m_audio_input_source_headset_mic);
+                }
+                break;
+            case st3m_audio_input_source_onboard_mic:
+                if (avail) {
+                    _audio_input_set_source(
+                        st3m_audio_input_source_onboard_mic);
+                }
+                break;
+        }
+    }
+}
+
+void st3m_audio_input_engine_set_source(st3m_audio_input_source_t source) {
+    LOCK;
+    state.engine_target_source = source;
+    UNLOCK;
+}
+
+static bool _check_thru_source_avail(st3m_audio_input_source_t source) {
+    bool avail = _check_engine_source_avail(source);
+    if ((source == st3m_audio_input_source_onboard_mic) &&
+        (!state.onboard_mic_to_speaker_allowed) &&
+        (!state.jacksense.headphones)) {
+        avail = false;
+    }
+
+    if ((state.engine_source != st3m_audio_input_source_none) &&
+        (state.engine_source != source)) {
+        avail = false;
+    }
+    return avail;
+}
+
+bool st3m_audio_input_thru_get_source_avail(st3m_audio_input_source_t source) {
+    bool ret = false;
+    if (source == st3m_audio_input_source_auto) {
+        LOCK;
+        ret = ret || _check_thru_source_avail(st3m_audio_input_source_line_in);
+        ret = ret ||
+              _check_thru_source_avail(st3m_audio_input_source_headset_mic);
+        ret = ret ||
+              _check_thru_source_avail(st3m_audio_input_source_onboard_mic);
+        UNLOCK;
+    } else {
+        LOCK;
+        ret = _check_thru_source_avail(source);
+        UNLOCK;
+    }
+    return ret;
+}
+
+void _update_thru_source() {
+    st3m_audio_input_source_t source;
+
+    LOCK;
+    source = state.thru_target_source;
+    if (source == st3m_audio_input_source_auto) {
+        if (state.engine_source != st3m_audio_input_source_none) {
+            source = state.engine_source;
+        } else if (_check_thru_source_avail(st3m_audio_input_source_line_in)) {
+            source = st3m_audio_input_source_line_in;
+        } else if (_check_thru_source_avail(
+                       st3m_audio_input_source_headset_mic)) {
+            source = st3m_audio_input_source_headset_mic;
+        } else if (_check_thru_source_avail(
+                       st3m_audio_input_source_onboard_mic)) {
+            source = st3m_audio_input_source_onboard_mic;
+        } else {
+            source = st3m_audio_input_source_none;
+        }
+    }
+
+    bool avail = _check_thru_source_avail(source);
+    UNLOCK;
+    source = avail ? source : st3m_audio_input_source_none;
+
+    bool return_flag = false;
+    LOCK;
+    if (state.engine_source != st3m_audio_input_source_none) {
+        if (state.engine_source == source) {
+            state.thru_source = state.engine_source;
+        } else {
+            state.thru_source = st3m_audio_input_source_none;
+        }
+        return_flag = true;
+    }
+    state.thru_source = source;
+    if (state.thru_source == state.source) {
+        return_flag = true;
+    }
+    UNLOCK;
+    if (return_flag) return;
+
+    switch (source) {
+        case st3m_audio_input_source_none:
+            if (avail) {
+                _audio_input_set_source(st3m_audio_input_source_none);
+            }
+            break;
+        case st3m_audio_input_source_line_in:
+            if (avail) {
+                _audio_input_set_source(st3m_audio_input_source_line_in);
+            }
+            break;
+        case st3m_audio_input_source_headset_mic:
+            if (avail) {
+                _audio_input_set_source(st3m_audio_input_source_headset_mic);
+            }
+            break;
+        case st3m_audio_input_source_onboard_mic:
+            if (avail) {
+                _audio_input_set_source(st3m_audio_input_source_onboard_mic);
+            }
+            break;
+        case st3m_audio_input_source_auto:
+            // should not be possible
+            break;
+    }
+}
+
+void st3m_audio_input_thru_set_source(st3m_audio_input_source_t source) {
+    LOCK;
+    state.thru_target_source = source;
+    UNLOCK;
+}
+
+void _update_sink() {
     flow3r_bsp_audio_jacksense_state_t st;
     flow3r_bsp_audio_read_jacksense(&st);
     static bool _speaker_eq_on_prev = false;
-
     // Update volume to trigger mutes if needed. But only do that if the
     // jacks actually changed.
     LOCK;
@@ -268,6 +530,13 @@ static void _update_jacksense() {
     UNLOCK;
 }
 
+static void _update_routing() {
+    // order important
+    _update_sink();
+    _update_engine_source();
+    _update_thru_source();
+}
+
 void st3m_audio_player_function_dummy(int16_t *rx, int16_t *tx, uint16_t len) {
     for (uint16_t i = 0; i < len; i++) {
         tx[i] = 0;
@@ -281,9 +550,7 @@ void st3m_audio_init(void) {
 
     flow3r_bsp_audio_init();
 
-    st3m_audio_input_thru_set_volume_dB(-20);
-    state.speaker_eq_on = true;
-    _update_jacksense();
+    _update_routing();
     _output_apply(&state.speaker);
     _output_apply(&state.headphones);
     bool _speaker_eq = (!_headphones_connected()) && state.speaker_eq_on;
@@ -300,11 +567,13 @@ static void _audio_player_task(void *data) {
 
     int16_t buffer_tx[FLOW3R_BSP_AUDIO_DMA_BUFFER_SIZE * 2];
     int16_t buffer_rx[FLOW3R_BSP_AUDIO_DMA_BUFFER_SIZE * 2];
+    int16_t buffer_rx_dummy[FLOW3R_BSP_AUDIO_DMA_BUFFER_SIZE * 2];
     memset(buffer_tx, 0, sizeof(buffer_tx));
     memset(buffer_rx, 0, sizeof(buffer_rx));
+    memset(buffer_rx_dummy, 0, sizeof(buffer_rx));
     size_t count;
 
-    bool hwmute = flow3r_bsp_audio_has_hardware_mute();
+    st3m_audio_input_source_t source_prev = st3m_audio_input_source_none;
 
     while (true) {
         count = 0;
@@ -321,39 +590,88 @@ static void _audio_player_task(void *data) {
         }
 
         LOCK;
+        st3m_audio_input_source_t source = state.source;
+        st3m_audio_input_source_t engine_source = state.engine_source;
+        st3m_audio_input_source_t thru_source = state.thru_source;
         bool headphones = _headphones_connected();
         st3m_audio_player_function_t function = state.function;
         int32_t software_volume = headphones ? state.headphones.volume_software
                                              : state.speaker.volume_software;
-        bool software_mute =
-            headphones ? state.headphones.mute : state.speaker.mute;
         bool input_thru_mute = state.input_thru_mute;
         int32_t input_thru_vol_int = state.input_thru_vol_int;
         UNLOCK;
 
-        (*function)(buffer_rx, buffer_tx, FLOW3R_BSP_AUDIO_DMA_BUFFER_SIZE * 2);
-        for (uint16_t i = 0; i < FLOW3R_BSP_AUDIO_DMA_BUFFER_SIZE; i++) {
-            st3m_scope_write(buffer_tx[2 * i] >> 2);
+        // <RX SIGNAL PREPROCESSING>
+
+        int16_t rx_gain = 256;  // unity
+        uint8_t rx_chan = 3;    // stereo = 0; left = 1; right = 2; off = 3;
+        if (source != source_prev) {
+            // state change: throw away buffer
+            source_prev = source;
+            memset(buffer_rx, 0, sizeof(buffer_rx));
+        } else if (source == st3m_audio_input_source_headset_mic) {
+            // headset has its own gain thing going on, leave at unity
+            rx_chan = 1;
+        } else if (source == st3m_audio_input_source_line_in) {
+            LOCK;
+            int16_t gain = state.line_in_gain_software;
+            UNLOCK;
+            rx_gain = gain;
+            rx_chan = 0;
+        } else if (source == st3m_audio_input_source_onboard_mic) {
+            LOCK;
+            int16_t gain = state.onboard_mic_gain_software;
+            UNLOCK;
+            rx_gain = gain;
+            rx_chan = 1;
         }
 
-        if (!hwmute && software_mute) {
-            // Software muting needed. Only used on P1.
-            for (int i = 0; i < (FLOW3R_BSP_AUDIO_DMA_BUFFER_SIZE * 2);
-                 i += 2) {
-                buffer_tx[i] = 0;
+        if (rx_chan == 0) {
+            // keep stereo image
+            for (uint16_t i = 0; i < FLOW3R_BSP_AUDIO_DMA_BUFFER_SIZE * 2;
+                 i++) {
+                buffer_rx[i] = (((int32_t)buffer_rx[i]) * rx_gain) >> 8;
             }
+        } else if (rx_chan < 3) {
+            // mix one of the input channels to both rx stereo chans (easier
+            // mono sources)
+            for (uint16_t i = 0; i < FLOW3R_BSP_AUDIO_DMA_BUFFER_SIZE * 2;
+                 i++) {
+                uint16_t j = (i / 2) * 2 + rx_chan - 1;
+                buffer_rx[i] = (((int32_t)buffer_rx[j]) * rx_gain) >> 8;
+            }
+        }
+
+        int16_t *engine_rx;
+
+        if (engine_source == st3m_audio_input_source_none) {
+            engine_rx = buffer_rx_dummy;
         } else {
-            for (int i = 0; i < (FLOW3R_BSP_AUDIO_DMA_BUFFER_SIZE * 2);
-                 i += 1) {
-                int32_t acc = buffer_tx[i];
+            engine_rx = buffer_rx;
+        }
 
-                acc = (acc * software_volume) >> 15;
+        // <ACTUAL ENGINE CALL>
 
-                if (!input_thru_mute) {
-                    acc += (((int32_t)buffer_rx[i]) * input_thru_vol_int) >> 15;
-                }
-                buffer_tx[i] = acc;
+        (*function)(engine_rx, buffer_tx, FLOW3R_BSP_AUDIO_DMA_BUFFER_SIZE * 2);
+
+        // </ACTUAL ENGINE CALL>
+
+        for (uint16_t i = 0; i < FLOW3R_BSP_AUDIO_DMA_BUFFER_SIZE; i++) {
+            st3m_scope_write(buffer_tx[2 * i] >> 2);
+        }
+
+        for (int i = 0; i < (FLOW3R_BSP_AUDIO_DMA_BUFFER_SIZE * 2); i += 1) {
+            int32_t acc = buffer_tx[i];
+
+            acc = (acc * software_volume) >> 15;
+
+            if ((thru_source != st3m_audio_input_source_none) &&
+                ((engine_source == thru_source) ||
+                 (engine_source == st3m_audio_input_source_none)) &&
+                (!input_thru_mute)) {
+                acc += (((int32_t)buffer_rx[i]) * input_thru_vol_int) >> 15;
             }
+            buffer_tx[i] = acc;
         }
 
         flow3r_bsp_audio_write(buffer_tx, sizeof(buffer_tx), &count, 1000);
@@ -371,21 +689,24 @@ static void _jacksense_update_task(void *data) {
     TickType_t last_wake = xTaskGetTickCount();
     while (1) {
         vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(50));  // 20 Hz
-        _update_jacksense();
+        _update_routing();
     }
 }
 
 // BSP wrappers that don't need locking.
 
 void st3m_audio_headphones_line_in_set_hardware_thru(bool enable) {
+    return;
     flow3r_bsp_audio_headphones_line_in_set_hardware_thru(enable);
 }
 
 void st3m_audio_speaker_line_in_set_hardware_thru(bool enable) {
+    return;
     flow3r_bsp_audio_speaker_line_in_set_hardware_thru(enable);
 }
 
 void st3m_audio_line_in_set_hardware_thru(bool enable) {
+    return;
     flow3r_bsp_audio_line_in_set_hardware_thru(enable);
 }
 
@@ -412,44 +733,62 @@ GETTER(float, audio_headphones_get_maximum_volume_dB,
 GETTER(float, audio_speaker_get_maximum_volume_dB, state.speaker.volume_max)
 GETTER(bool, audio_headphones_get_mute, state.headphones.mute)
 GETTER(bool, audio_speaker_get_mute, state.speaker.mute)
+GETTER(st3m_audio_input_source_t, audio_input_engine_get_source,
+       state.engine_source)
+GETTER(st3m_audio_input_source_t, audio_input_thru_get_source,
+       state.thru_source)
+GETTER(st3m_audio_input_source_t, audio_input_engine_get_target_source,
+       state.engine_target_source)
+GETTER(st3m_audio_input_source_t, audio_input_thru_get_target_source,
+       state.thru_target_source)
 GETTER(st3m_audio_input_source_t, audio_input_get_source, state.source)
-GETTER(uint8_t, audio_headset_get_gain_dB, state.headset_gain)
+GETTER(float, audio_headset_mic_get_gain_dB, state.headset_mic_gain_dB)
+GETTER(float, audio_onboard_mic_get_gain_dB, state.onboard_mic_gain_dB)
+GETTER(float, audio_line_in_get_gain_dB, state.line_in_gain_dB)
 GETTER(float, audio_input_thru_get_volume_dB, state.input_thru_vol)
 GETTER(bool, audio_input_thru_get_mute, state.input_thru_mute)
 GETTER(bool, audio_speaker_get_eq_on, state.speaker_eq_on)
+GETTER(bool, audio_headset_mic_get_allowed, state.headset_mic_allowed)
+GETTER(bool, audio_onboard_mic_get_allowed, state.onboard_mic_allowed)
+GETTER(bool, audio_line_in_get_allowed, state.line_in_allowed)
+GETTER(bool, audio_onboard_mic_to_speaker_get_allowed,
+       state.onboard_mic_to_speaker_allowed)
 #undef GETTER
 
 // Locked global API functions.
 
-void st3m_audio_headset_set_gain_dB(int8_t gain_dB) {
-    gain_dB = flow3r_bsp_audio_headset_set_gain_dB(gain_dB);
+float st3m_audio_headset_mic_set_gain_dB(float gain_dB) {
+    if (gain_dB > 42) gain_dB = 42;
+    if (gain_dB < -9) gain_dB = -9;
+    int8_t hw_gain = flow3r_bsp_audio_headset_set_gain_dB(gain_dB);
+    int16_t software_gain = 256. * expf((gain_dB - hw_gain) * NAT_LOG_DB);
     LOCK;
-    state.headset_gain = gain_dB;
+    state.headset_mic_gain_dB = gain_dB;
+    state.headset_mic_gain_software = software_gain;
     UNLOCK;
+    return gain_dB;
 }
 
-void st3m_audio_input_set_source(st3m_audio_input_source_t source) {
-    switch (source) {
-        case st3m_audio_input_source_none:
-            flow3r_bsp_audio_input_set_source(
-                flow3r_bsp_audio_input_source_none);
-            break;
-        case st3m_audio_input_source_line_in:
-            flow3r_bsp_audio_input_set_source(
-                flow3r_bsp_audio_input_source_line_in);
-            break;
-        case st3m_audio_input_source_headset_mic:
-            flow3r_bsp_audio_input_set_source(
-                flow3r_bsp_audio_input_source_headset_mic);
-            break;
-        case st3m_audio_input_source_onboard_mic:
-            flow3r_bsp_audio_input_set_source(
-                flow3r_bsp_audio_input_source_onboard_mic);
-            break;
-    }
+float st3m_audio_line_in_set_gain_dB(float gain_dB) {
+    if (gain_dB > 30) gain_dB = 30;
+    if (gain_dB < -24) gain_dB = -24;
+    int16_t software_gain = 256. * expf(gain_dB * NAT_LOG_DB);
     LOCK;
-    state.source = source;
+    state.line_in_gain_dB = gain_dB;
+    state.line_in_gain_software = software_gain;
     UNLOCK;
+    return gain_dB;
+}
+
+float st3m_audio_onboard_mic_set_gain_dB(float gain_dB) {
+    if (gain_dB > 42) gain_dB = 42;
+    if (gain_dB < 0) gain_dB = 0;
+    int16_t software_gain = 256. * expf(gain_dB * NAT_LOG_DB);
+    LOCK;
+    state.onboard_mic_gain_dB = gain_dB;
+    state.onboard_mic_gain_software = software_gain;
+    UNLOCK;
+    return gain_dB;
 }
 
 void st3m_audio_speaker_set_eq_on(bool enabled) {
@@ -464,10 +803,35 @@ void st3m_audio_input_thru_set_mute(bool mute) {
     UNLOCK;
 }
 
+void st3m_audio_headset_mic_set_allowed(bool allowed) {
+    LOCK;
+    state.headset_mic_allowed = allowed;
+    UNLOCK;
+}
+
+void st3m_audio_onboard_mic_set_allowed(bool allowed) {
+    LOCK;
+    state.onboard_mic_allowed = allowed;
+    UNLOCK;
+}
+
+void st3m_audio_line_in_set_allowed(bool allowed) {
+    LOCK;
+    state.line_in_allowed = allowed;
+    UNLOCK;
+}
+
+void st3m_audio_onboard_mic_to_speaker_set_allowed(bool allowed) {
+    LOCK;
+    state.onboard_mic_to_speaker_allowed = allowed;
+    UNLOCK;
+}
+
 float st3m_audio_input_thru_set_volume_dB(float vol_dB) {
     if (vol_dB > 0) vol_dB = 0;
+    int16_t vol = 32768. * expf(vol_dB * NAT_LOG_DB);
     LOCK;
-    state.input_thru_vol_int = (int32_t)(32768. * expf(vol_dB * NAT_LOG_DB));
+    state.input_thru_vol_int = vol;
     state.input_thru_vol = vol_dB;
     UNLOCK;
     return vol_dB;
diff --git a/components/st3m/st3m_audio.h b/components/st3m/st3m_audio.h
index cb8a21275a836e283fbf40f31231bec98a69c195..b8bb4bd8ef4adffcf3a25a4bb9cee9f7dc6694f8 100644
--- a/components/st3m/st3m_audio.h
+++ b/components/st3m/st3m_audio.h
@@ -10,7 +10,10 @@ typedef enum {
     // Headset microphone on left jack.
     st3m_audio_input_source_headset_mic = 2,
     // Onboard microphone (enabled red LED).
-    st3m_audio_input_source_onboard_mic = 3
+    st3m_audio_input_source_onboard_mic = 3,
+    // auto switching depending on availability
+    // line in preferred to headset mic preferred to onboard mic.
+    st3m_audio_input_source_auto = 4
 } st3m_audio_input_source_t;
 
 typedef void (*st3m_audio_player_function_t)(int16_t* tx, int16_t* rx,
@@ -151,20 +154,31 @@ void st3m_audio_line_in_set_hardware_thru(bool enable);
  * Note: The onboard digital mic turns on an LED on the top board if it receives
  * a clock signal which is considered a good proxy for its capability of reading
  * data.
- *
- * TODO: check if sources are available
  */
-void st3m_audio_input_set_source(st3m_audio_input_source_t source);
+void st3m_audio_input_engine_set_source(st3m_audio_input_source_t source);
+st3m_audio_input_source_t st3m_audio_input_engine_get_source(void);
+st3m_audio_input_source_t st3m_audio_input_engine_get_target_source(void);
+
+void st3m_audio_input_thru_set_source(st3m_audio_input_source_t source);
+st3m_audio_input_source_t st3m_audio_input_thru_get_source(void);
+st3m_audio_input_source_t st3m_audio_input_thru_get_target_source(void);
 
-/* Returns the currently selected input source.
- */
 st3m_audio_input_source_t st3m_audio_input_get_source(void);
 
-/* Hardware preamp gain, 0dB-50dB. TODO: figure out if int/float inconsistency
- * is a good thing here compared to all other _dB functions.
+/* Gain of headset mic source
+ */
+float st3m_audio_headset_mic_set_gain_dB(float gain_dB);
+float st3m_audio_headset_mic_get_gain_dB(void);
+
+/* Gain of onboard mic source
+ */
+float st3m_audio_onboard_mic_set_gain_dB(float gain_dB);
+float st3m_audio_onboard_mic_get_gain_dB(void);
+
+/* Gain of line in source
  */
-void st3m_audio_headset_set_gain_dB(int8_t gain_dB);
-uint8_t st3m_audio_headset_get_gain_dB(void);
+float st3m_audio_line_in_set_gain_dB(float gain_dB);
+float st3m_audio_line_in_get_gain_dB(void);
 
 /* You can route whatever source is selected with st3m_audio_input_set_source to
  * the audio output. Use these to control volume and mute.
@@ -180,6 +194,21 @@ bool st3m_audio_input_thru_get_mute(void);
 void st3m_audio_speaker_set_eq_on(bool enable);
 bool st3m_audio_speaker_get_eq_on(void);
 
+void st3m_audio_headset_mic_set_allowed(bool allowed);
+bool st3m_audio_headset_mic_get_allowed(void);
+
+void st3m_audio_onboard_mic_set_allowed(bool allowed);
+bool st3m_audio_onboard_mic_get_allowed(void);
+
+void st3m_audio_line_in_set_allowed(bool allowed);
+bool st3m_audio_line_in_get_allowed(void);
+
+void st3m_audio_onboard_mic_to_speaker_set_allowed(bool allowed);
+bool st3m_audio_onboard_mic_to_speaker_get_allowed(void);
+
+bool st3m_audio_input_engine_get_source_avail(st3m_audio_input_source_t source);
+bool st3m_audio_input_thru_get_source_avail(st3m_audio_input_source_t source);
+
 /*
 HEADPHONE PORT POLICY
 
diff --git a/docs/api/audio.rst b/docs/api/audio.rst
index 6225695299fab03dd8b1cecb9907380d40bfeabc..e2cf8103cfceedffdc5bf9cca1e8a8c5c6cc65db 100644
--- a/docs/api/audio.rst
+++ b/docs/api/audio.rst
@@ -3,9 +3,11 @@
 ``audio`` module
 ================
 
-Many of these functions are available in three variants: headphone volume,
-speaker volume, and volume. If :code:`headphones_are_connected()` returns 1
-the "headphone" variant is chosen, else the "speaker" variant is chosen.
+The audio module provides the backbone for handling basic audio bookkeeping such as volume and signal routing.
+Actual sound is created by the engines, i.e. bl00mbox and media player at the moment.
+
+Jack Detection
+--------------
 
 .. py:function:: headset_is_connected() -> bool
 
@@ -22,6 +24,132 @@ the "headphone" variant is chosen, else the "speaker" variant is chosen.
    Returns 1 if the line-in jack was connected at the last call
    of audio_update_jacksense.
 
+Input Sources
+-------------
+
+.. note::
+    The onboard digital mic turns on an LED on the top board if it receives
+    a clock signal which is considered a good proxy for its capability of reading
+    data. Access to the onboard mic can be disabled entirely in the audio config
+    menu.
+
+The codec can receive data from the line in jack, the headset mic pin in the headphone jack, or the
+internal microphone. We distinguish between two use cases: 1) sending the signal to the audio engines
+to be mangled with, and 2) directly mix it to the output with a variable volume level. We provide two
+different APIs for each use case.
+
+Sources may or may not be available; for line in and the headset mic they might simply not be plugged in,
+but also users can configure in the settings which inputs are available in the first place. To handle this
+uncertainity gracefully, instead of momentarily trying (and potentially failing) to set up a connection
+you set a desired target and the backend will attempt to connect to it continuously until the target is reset
+to none. The target states are not cleared when exiting applications, if you don't intend to also configure
+the source for other applications please reset them to whatever state you found them in like so:
+
+.. code-block:: python
+
+    on_enter(self, vm):
+        # save original target
+        self.orig_engine_target_source = audio.input_engine_get_target_source()
+        # switch to your preferred one
+        audio.input_engine_set_target_source(audio.INPUT_SOURCE_AUTO)
+
+    on_exit(self):
+        # restore original target
+        audio.input_engine_set_source(self.orig_engine_target_source)
+
+Since the codec can only send data from one source at a time. in case of a disagreement between the engine
+source and the thru source, the engine source wins and the through source is temporarily set to none.
+For thru to follow the engine source if available and not none use ``audio.input_thru_set_source(audio.INPUT_SOURCE_AUTO)``.
+
+The available sources for both engine and thru are slightly different: The engine only looks for permissions
+and hardware state, while thru can not access the onboard mic if playback is happening through the speakers.
+This is set up to prevent accidential feedback loops. However the user can give permission to acces this mode
+in the user config.
+
+
+.. py:data:: INPUT_SOURCE_NONE
+
+    No source, datastream suspended.
+    
+.. py:data:: INPUT_SOURCE_LINE_IN
+
+    Stream data from line in if available.
+
+.. py:data:: INPUT_SOURCE_HEADSET_MIC
+
+    Stream data from headset mic if available and allowed.
+
+.. py:data:: INPUT_SOURCE_ONBOARD_MIC
+
+    Stream data from onboard mic if allowed.
+
+.. py:data:: INPUT_SOURCE_AUTO
+
+    Stream data from available input, line in is preferred to headset mic is preferred to onboard mic.
+    For ``input_thru_source`` matching ``input_engine_source`` is preferred to line in.
+
+.. py:function:: input_engine_set_source(source : int) -> int
+
+    Set up a continuous connection query for routing the given source to the input for the audio engines.
+    Check for success with ``input_engine_get_source()`` and clean up by passing ``INPUT_SOURCE_NONE``
+
+.. py:function:: input_engine_get_target_source() -> int
+
+    Returns target source last set with input_engine_set_source.
+
+.. py:function:: input_engine_get_source() -> int
+
+    Returns source currently connected to the audio engines.
+
+.. py:function:: input_engine_get_source_avail(source : int) -> bool
+
+    Returns true if it is currently possible to connect the audio engines to a given source.
+    If given ``INPUT_SOURCE_AUTO`` returns true if any source can be connected to the engines.
+
+.. py:function:: input_thru_set_source(source : int) -> int
+
+    Set up a continuous connection query for routing the given source to the output mixer of the codec.
+    Check for success with ``input_thru_get_source()`` and clean up by passing ``INPUT_SOURCE_NONE``
+
+.. py:function:: input_thru_get_target_source() -> int
+
+    Returns target source last set with input_thru_set_source.
+
+.. py:function:: input_thru_get_source() -> int
+
+    Returns the source currently mixed directly to output.
+
+.. py:function:: input_get_source() -> int
+
+    Returns the source the codec is connected to at the moment.
+
+.. py:function:: input_thru_get_source_avail(source : int) -> bool
+
+    Returns true if it is currently possible to a given source to thru.
+    If given ``INPUT_SOURCE_AUTO`` returns true if any source can be connected to thru.
+
+.. py:function:: input_thru_set_volume_dB(vol_dB : float)
+.. py:function:: input_thru_get_volume_dB() -> float
+.. py:function:: input_thru_set_mute(mute : bool)
+.. py:function:: input_thru_get_mute() -> bool
+
+    Volume and mute control for input_thru. Please don't use this as a replacement for terminating
+    a connection, ``input_thru_set_source(audio.INPUT_SOURCE_NONE)`` instead!
+
+.. py:function:: input_line_in_get_allowed(mute : bool)
+.. py:function:: input_headset_mic_get_allowed(mute : bool)
+.. py:function:: input_onboard_mic_get_allowed(mute : bool)
+.. py:function:: input_onboard_mic_to_speaker_get_allowed(mute : bool)
+
+    Returns if the user has forbidden access to the resource.
+
+OS development
+--------------
+
+Many of these functions are available in three variants: headphone volume,
+speaker volume, and volume. If :code:`headphones_are_connected()` returns 1
+the "headphone" variant is chosen, else the "speaker" variant is chosen.
+
 .. py:function:: headphones_detection_override(enable : bool)
 
    If a sleeve contact mic doesn't pull the detection pin low enough the
@@ -102,49 +230,19 @@ the "headphone" variant is chosen, else the "speaker" variant is chosen.
    :code:`audio_{headphones_/speaker_/}set_{maximum/minimum}_volume_` and 0 if
    in a fake mute condition.
 
-.. py:function:: headphones_line_in_set_hardware_thru(enable : bool)
-.. py:function:: speaker_line_in_set_hardware_thru(enable : bool)
-.. py:function:: line_in_set_hardware_thru(enable : bool)
-
-   These route whatever is on the line in port directly to the headphones or
-   speaker respectively (enable = 1), or don't (enable = 0). Is affected by mute
-   and coarse hardware volume settings, however software fine volume is not
-   applied.
-
-   Good for testing, might deprecate later, idk~
-
-.. py:function:: input_set_source(source : int)
-.. py:function:: input_get_source() -> int
-
-   The codec can transmit audio data from different sources. This function
-   enables one or no source as provided by the ``INPUT_SOURCE_*`` constants.
-
-   Note: The onboard digital mic turns on an LED on the top board if it receives
-   a clock signal which is considered a good proxy for its capability of reading
-   data.
-
-.. py:data:: INPUT_SOURCE_NONE
-.. py:data:: INPUT_SOURCE_LINE_IN
-.. py:data:: INPUT_SOURCE_HEADSET_MIC
-.. py:data:: INPUT_SOURCE_ONBOARD_MIC
-
-.. py:function:: headset_set_gain_dB(gain_dB : int)
-.. py:function:: headset_get_gain_dB() -> int
-
-   Hardware preamp gain, 0dB-50dB. TODO: figure out if int/float inconsistency
-   is a good thing here compared to all other _dB functions.
-
-.. py:function:: input_thru_set_volume_dB(vol_dB : float)
-.. py:function:: input_thru_get_volume_dB() -> float
-.. py:function:: input_thru_set_mute(mute : bool)
-.. py:function:: input_thru_get_mute() -> bool
+.. py:function:: headset_mic_set_gain_dB(gain_dB : float)
+.. py:function:: headset_mic_get_gain_dB() -> float
+.. py:function:: onboard_mic_set_gain_dB(gain_dB : float)
+.. py:function:: onboard_mic_get_gain_dB() -> float
+.. py:function:: line_in_set_gain_dB(gain_dB : float)
+.. py:function:: line_in_get_gain_dB() -> float
 
-   You can route whatever source is selected with input_set_source() to
-   the audio output. Use these to control volume and mute.
+   Set and get gain for the respective input channels.
 
 .. py:function:: codec_i2c_write(reg : int, data : int)
 
-   Write audio codec register. Obviously very unsafe. Have fun.
+   Write audio codec register. Obviously very unsafe. Do not use in applications that you
+   distribute to users. This can fry your speakers with DC>
 
 
 Headphone port policy
diff --git a/python_payload/apps/audio_config/__init__.py b/python_payload/apps/audio_config/__init__.py
index 3045e4fe82bed567775eacd96a451ea7278026c3..3d2e73dbe854066cca76522eb69e3b3a44811db1 100644
--- a/python_payload/apps/audio_config/__init__.py
+++ b/python_payload/apps/audio_config/__init__.py
@@ -13,49 +13,69 @@ class Drawable:
         self.y = 0
         self.font_size = 20
         self.active = False
-        # override these w ur own val :3
-        self.focused_widget = 2
         self.mid_x = 30
         self.num_widgets = 2
         self.overhang = -70
         self.line_height = 24
         self.ctx = None
         self.press = press
-
-    def draw_heading(self, label):
+        self.focus_pos_limit_min = -60
+        self.focus_pos_limit_max = 60
+        self.focus_pos_limit_first = -60
+        self.focus_pos_limit_last = 80
+        self.first_widget_pos = 0
+        self.last_widget_pos = 0
+        self.focus_widget_pos_min = 0
+        self.focus_widget_pos_max = 0
+        self._focus_widget = 2
+        self._focus_widget_prev = 1
+
+    @property
+    def focus_widget(self):
+        return self._focus_widget
+
+    @property
+    def focus_widget_prev(self):
+        return self._focus_widget_prev
+
+    @focus_widget.setter
+    def focus_widget(self, val):
+        if val < 2:
+            val = 2
+        if val > self.num_widgets - 1:
+            val = self.num_widgets - 1
+        self._focus_widget_prev = self._focus_widget
+        self._focus_widget = val
+
+    @property
+    def at_first_widget(self):
+        return self.focus_widget <= 2
+
+    @property
+    def at_last_widget(self):
+        return self.focus_widget >= (self.num_widgets - 1)
+
+    def draw_heading(self, label, col=(0.8, 0.8, 0.8), embiggen=6, margin=2):
         ctx = self.ctx
         if ctx is None:
             return
         self.widget_no += 1
-        if not self.active:
-            if self.press.select_pressed and self.focused_widget > 0:
-                self.active = True
-                self.press.select_pressed = False
-            elif self.press.left_pressed:
-                self.focused_widget -= 1
-                if self.focused_widget < 2:
-                    self.focused_widget = 2
-                self.press.left_pressed = False
-            elif self.press.right_pressed:
-                self.focused_widget += 1
-                if self.focused_widget > self.num_widgets - 1:
-                    self.focused_widget = self.num_widgets - 1
-                self.press.right_pressed = False
-        if self.widget_no == self.focused_widget and not self.active:
-            ctx.rectangle(-130, int(self.y - self.font_size * 0.8), 260, self.font_size)
-            ctx.line_width = 2.0
-            ctx.rgba(*colours.GO_GREEN, 1.0)
-            ctx.stroke()
+        self.y += embiggen + margin
+        if self.widget_no == self.focus_widget:
+            if self.focus_widget > self.focus_widget_prev:
+                self.focus_widget += 1
+            else:
+                self.focus_widget -= 1
         ctx.gray(1)
         ctx.move_to(self.mid_x, self.y)
         ctx.save()
-        ctx.rgb(0.8, 0.8, 0.8)
+        ctx.rgb(*col)
         ctx.move_to(0, self.y)
         ctx.text_align = ctx.CENTER
-        ctx.font_size += 6
+        ctx.font_size += embiggen
         ctx.text(label)
         ctx.restore()
-        self.y += self.line_height + 8
+        self.y += self.line_height + embiggen + margin
 
     def draw_widget(self, label):
         ctx = self.ctx
@@ -63,24 +83,25 @@ class Drawable:
             return
         self.widget_no += 1
         if not self.active:
-            if self.press.select_pressed and self.focused_widget > 0:
+            if self.press.select_pressed and self.focus_widget > 0:
                 self.active = True
                 self.press.select_pressed = False
             elif self.press.left_pressed:
-                self.focused_widget -= 1
-                if self.focused_widget < 2:
-                    self.focused_widget = 2
+                self.focus_widget -= 1
                 self.press.left_pressed = False
             elif self.press.right_pressed:
-                self.focused_widget += 1
-                if self.focused_widget > self.num_widgets - 1:
-                    self.focused_widget = self.num_widgets - 1
+                self.focus_widget += 1
                 self.press.right_pressed = False
-        if self.widget_no == self.focused_widget and not self.active:
-            ctx.rectangle(-130, int(self.y - self.font_size * 0.8), 260, self.font_size)
-            ctx.line_width = 2.0
-            ctx.rgba(*colours.GO_GREEN, 1.0)
-            ctx.stroke()
+        if self.widget_no == self.focus_widget:
+            self.focus_widget_pos_min = self.y
+            if not self.active:
+                ctx.rectangle(
+                    -130, int(self.y - self.font_size * 0.8), 260, self.font_size
+                )
+                ctx.line_width = 2.0
+                ctx.rgba(*colours.GO_GREEN, 1.0)
+                ctx.stroke()
+            self.focus_widget_pos_max = self.y + self.line_height
         ctx.gray(1)
         ctx.move_to(self.mid_x, self.y)
         ctx.save()
@@ -95,7 +116,7 @@ class Drawable:
         if ctx is None:
             return
         self.draw_widget(label)
-        if self.widget_no == self.focused_widget and self.active:
+        if self.widget_no == self.focus_widget and self.active:
             if self.press.left_pressed:
                 no -= 1
                 if no < 0:
@@ -108,7 +129,7 @@ class Drawable:
                 self.active = False
                 self.press.select_pressed = False
         for a in range(len(choices)):
-            if a == no and self.active and self.widget_no == self.focused_widget:
+            if a == no and self.active and self.widget_no == self.focus_widget:
                 ctx.save()
                 ctx.rgba(*colours.GO_GREEN, 1.0)
                 ctx.rectangle(
@@ -135,13 +156,13 @@ class Drawable:
                 ctx.text(choices[a] + " ")
         return no
 
-    def draw_number(self, label, step_size, no, unit=""):
+    def draw_number(self, label, step_size, no, unit="", val_col=(0.8, 0.8, 0.8)):
         ctx = self.ctx
         if ctx is None:
             return
         self.draw_widget(label)
         ret = no
-        if self.widget_no == self.focused_widget and self.active:
+        if self.widget_no == self.focus_widget and self.active:
             if self.press.left_pressed:
                 ret -= step_size
             elif self.press.right_pressed:
@@ -150,7 +171,7 @@ class Drawable:
                 self.active = False
                 self.press.select_pressed = False
 
-        if self.active and self.widget_no == self.focused_widget:
+        if self.active and self.widget_no == self.focus_widget:
             ctx.save()
             ctx.rgba(*colours.GO_GREEN, 1.0)
             ctx.rectangle(
@@ -160,24 +181,57 @@ class Drawable:
                 self.font_size,
             ).stroke()
             ctx.restore()
-            ctx.text(str(no)[:4] + unit)
-        else:
-            ctx.text(str(no)[:4] + unit)
+
+        ctx.save()
+        ctx.rgb(*val_col)
+        ctx.text(str(no)[:4] + unit)
+        ctx.restore()
         return ret
 
-    def draw_boolean(self, label, value, on_str="on", off_str="off"):
+    def draw_boolean(
+        self,
+        label,
+        value,
+        on_str="on",
+        off_str="off",
+        val_col=(0.8, 0.8, 0.8),
+        on_hint=None,
+        off_hint=None,
+    ):
         ctx = self.ctx
         if ctx is None:
             return
         self.draw_widget(label)
-        if self.widget_no == self.focused_widget and self.active:
+        if self.widget_no == self.focus_widget and self.active:
             value = not value
             self.active = False
 
+        ctx.save()
+        ctx.rgb(*val_col)
         if value:
             ctx.text(on_str)
         else:
             ctx.text(off_str)
+        ctx.restore()
+        if self.widget_no == self.focus_widget:
+            if value:
+                hint = on_hint
+            else:
+                hint = off_hint
+            if hint is not None:
+                ctx.save()
+                ctx.font_size -= 4
+                ctx.text_align = ctx.CENTER
+                ctx.rgb(0.9, 0.9, 0.9)
+                lines = hint.split("\n")
+                self.y -= 3
+                for line in lines:
+                    ctx.move_to(0, self.y)
+                    ctx.text(line)
+                    self.y += self.line_height - 5
+                ctx.restore()
+                if self.y > 115:
+                    self.focus_widget_pos_max = self.y
         return value
 
     def draw_bg(self):
@@ -186,25 +240,29 @@ class Drawable:
             return
         ctx.gray(1.0)
         ctx.move_to(-100, -80)
-        wig = self.focused_widget - 1
-        if wig < 2:
-            wig = 2
-        if wig > self.num_widgets - 3:
-            wig = self.num_widgets - 3
-        focus_pos = self.overhang + (wig - 0.5) * self.line_height
-        if focus_pos > 40:
-            self.overhang -= 7
-        if focus_pos < -40:
-            self.overhang += 7
+        scroll_val = 0
+        scroll_speed = 7
+        if self.at_last_widget:
+            if self.focus_widget_pos_max > self.focus_pos_limit_last:
+                scroll_val = -self.focus_widget_pos_max + self.focus_pos_limit_last
+        elif self.at_first_widget:
+            if self.focus_widget_pos_min < self.focus_pos_limit_first:
+                scroll_val = 9999
+        elif self.focus_widget_pos_max > self.focus_pos_limit_max:
+            scroll_val = -9999
+        elif self.focus_widget_pos_min < self.focus_pos_limit_min:
+            scroll_val = 9999
+
+        if scroll_val > 0:
+            self.overhang += min(scroll_val, scroll_speed)
+        else:
+            self.overhang += max(scroll_val, -scroll_speed)
+
         self.y = self.overhang
         self.widget_no = 0
         ctx.rectangle(-120, -120, 240, 240)
         ctx.gray(0)
         ctx.fill()
-        ctx.save()
-        ctx.font_size = 20
-        ctx.gray(0.8)
-        ctx.restore()
         ctx.font_size = self.font_size
 
 
@@ -226,9 +284,12 @@ class SpeakerMenu(Submenu):
     def __init__(self, press):
         super().__init__(press)
         self.num_widgets = 6
-        self.focused_widget = 2
         self.overhang = -40
         self.mid_x = 50
+        self.focus_pos_limit_min = -100
+        self.focus_pos_limit_max = 100
+        self.focus_pos_limit_first = -100
+        self.focus_pos_limit_last = 100
 
     def _draw(self, ctx):
         self.ctx = ctx
@@ -272,9 +333,12 @@ class HeadphonesMenu(Submenu):
     def __init__(self, press):
         super().__init__(press)
         self.num_widgets = 5
-        self.focused_widget = 2
         self.overhang = -40
         self.mid_x = 50
+        self.focus_pos_limit_min = -100
+        self.focus_pos_limit_max = 100
+        self.focus_pos_limit_first = -100
+        self.focus_pos_limit_last = 100
 
     def _draw(self, ctx):
         self.ctx = ctx
@@ -316,9 +380,12 @@ class VolumeControlMenu(Submenu):
     def __init__(self, press):
         super().__init__(press)
         self.num_widgets = 6
-        self.focused_widget = 2
         self.overhang = -40
         self.mid_x = 25
+        self.focus_pos_limit_min = -100
+        self.focus_pos_limit_max = 100
+        self.focus_pos_limit_first = -100
+        self.focus_pos_limit_last = 100
 
     def _draw(self, ctx):
         self.ctx = ctx
@@ -370,21 +437,121 @@ class VolumeControlMenu(Submenu):
 class InputMenu(Submenu):
     def __init__(self, press):
         super().__init__(press)
-        self.num_widgets = 6
-        self.focused_widget = 2
-        self.overhang = -40
-        self.mid_x = 50
+        self.num_widgets = 11
+        self.overhang = -85
+        self.mid_x = 0
 
     def _draw(self, ctx):
         self.ctx = ctx
         self.draw_bg()
-        self.draw_heading("inputs")
+
+        avail_col = (0.0, 0.9, 0.6)
+        warn_col = (0.9, 0.0, 0.0)
+        allow_col = (0.0, 0.7, 0.5)
+        not_allow_col = (0.8, 0.3, 0.3)
+        not_avail_col = (0.6, 0.6, 0.6)
+
+        self.draw_heading("line in", embiggen=5, margin=0)
+        if audio.line_in_get_allowed():
+            if audio.input_engine_get_source_avail(audio.INPUT_SOURCE_LINE_IN):
+                col = avail_col
+            else:
+                col = allow_col
+        else:
+            col = not_allow_col
+        tmp = self.draw_boolean(
+            "line in",
+            settings.onoff_line_in_allowed.value,
+            on_str="allowed",
+            off_str="blocked",
+            val_col=col,
+        )
+        if settings.onoff_line_in_allowed.value != tmp:
+            audio.line_in_set_allowed(tmp)
+            settings.onoff_line_in_allowed.set_value(tmp)
+
+        tmp = self.draw_number(
+            "gain",
+            1.5,
+            float(settings.num_line_in_gain_db.value),
+            unit="dB",
+        )
+        if settings.num_line_in_gain_db.value != tmp:
+            audio.line_in_set_gain_dB(tmp)
+            settings.num_line_in_gain_db.set_value(audio.line_in_get_gain_dB())
+
+        self.draw_heading("headset mic", embiggen=5, margin=0)
+
+        if audio.headset_mic_get_allowed():
+            if audio.input_engine_get_source_avail(audio.INPUT_SOURCE_HEADSET_MIC):
+                col = avail_col
+            else:
+                col = allow_col
+        else:
+            col = not_allow_col
+        tmp = self.draw_boolean(
+            "access",
+            settings.onoff_headset_mic_allowed.value,
+            on_str="allowed",
+            off_str="blocked",
+            val_col=col,
+        )
+        if settings.onoff_headset_mic_allowed.value != tmp:
+            audio.headset_mic_set_allowed(tmp)
+            settings.onoff_headset_mic_allowed.set_value(tmp)
+
         tmp = self.draw_number(
-            "headset gain", 1, int(settings.num_headset_gain_db.value), unit="dB"
+            "gain",
+            1.5,
+            float(settings.num_headset_mic_gain_db.value),
+            unit="dB",
+        )
+        if settings.num_headset_mic_gain_db.value != tmp:
+            tmp = audio.headset_mic_set_gain_dB(tmp)
+            settings.num_headset_mic_gain_db.set_value(tmp)
+
+        self.draw_heading("onboard mic", embiggen=5, margin=0)
+
+        if audio.onboard_mic_get_allowed():
+            col = avail_col
+        else:
+            col = not_allow_col
+        tmp = self.draw_boolean(
+            "access",
+            settings.onoff_onboard_mic_allowed.value,
+            on_str="allowed",
+            off_str="blocked",
+            val_col=col,
+        )
+        if settings.onoff_onboard_mic_allowed.value != tmp:
+            audio.onboard_mic_set_allowed(tmp)
+            settings.onoff_onboard_mic_allowed.set_value(tmp)
+
+        tmp = self.draw_number(
+            "gain",
+            1.5,
+            float(settings.num_onboard_mic_gain_db.value),
+            unit="dB",
+        )
+        if settings.num_onboard_mic_gain_db.value != tmp:
+            tmp = audio.onboard_mic_set_gain_dB(tmp)
+            settings.num_onboard_mic_gain_db.set_value(tmp)
+
+        if not audio.onboard_mic_to_speaker_get_allowed():
+            col = not_allow_col
+        else:
+            col = warn_col
+        tmp = self.draw_boolean(
+            "thru",
+            settings.onoff_onboard_mic_to_speaker_allowed.value,
+            on_str="allow",
+            off_str="phones",
+            val_col=col,
+            on_hint=" /!\ feedback possible /!\ ",
         )
-        if settings.num_headset_gain_db.value != tmp:
-            audio.headset_set_gain_dB(int(tmp))
-            settings.num_headset_gain_db.set_value(audio.headset_get_gain_dB())
+        if settings.onoff_onboard_mic_to_speaker_allowed.value != tmp:
+            audio.onboard_mic_to_speaker_set_allowed(tmp)
+            settings.onoff_onboard_mic_to_speaker_allowed.set_value(tmp)
 
 
 class Press:
diff --git a/python_payload/apps/audio_passthrough/__init__.py b/python_payload/apps/audio_passthrough/__init__.py
index 9614e0be48f00f127b69ed7fb7733e76418dab1a..f9802882da492192c98b2a223329ac989181e5c0 100644
--- a/python_payload/apps/audio_passthrough/__init__.py
+++ b/python_payload/apps/audio_passthrough/__init__.py
@@ -8,14 +8,14 @@ import math
 
 
 # Assume this is an enum
-ForceModes = ["AUTO", "FORCE_LINE_IN", "FORCE_LINE_OUT", "FORCE_MIC", "FORCE_NONE"]
+ForceModes = ["AUTO", "FORCE_LINE_IN", "FORCE_LINE_OUT", "FORCE_MIC"]
 
 
 STATE_TEXT: dict[int, str] = {
-    audio.INPUT_SOURCE_HEADSET_MIC: "using headset mic (line out)",
-    audio.INPUT_SOURCE_LINE_IN: "using line in",
-    audio.INPUT_SOURCE_ONBOARD_MIC: "using onboard mic",
-    audio.INPUT_SOURCE_NONE: "plug cable to line in/out",
+    audio.INPUT_SOURCE_AUTO: "auto",
+    audio.INPUT_SOURCE_HEADSET_MIC: "headset mic",
+    audio.INPUT_SOURCE_LINE_IN: "line in",
+    audio.INPUT_SOURCE_ONBOARD_MIC: "onboard mic",
 }
 
 
@@ -25,6 +25,9 @@ class AudioPassthrough(Application):
         self._button_0_pressed = False
         self._button_5_pressed = False
         self._force_mode: str = "AUTO"
+        self._mute = True
+        self._source = None
+        self.target_source = audio.INPUT_SOURCE_AUTO
 
     def on_enter(self, vm: Optional[ViewManager]) -> None:
         super().on_enter(vm)
@@ -53,36 +56,42 @@ class AudioPassthrough(Application):
         ctx.font_size = 25
         ctx.move_to(0, 0)
         ctx.save()
-        if audio.input_thru_get_mute():
+        if self._mute:
             # 0xff4500, red
             ctx.rgb(1, 0.41, 0)
         else:
             # 0x3cb043, green
             ctx.rgb(0.24, 0.69, 0.26)
-        ctx.text("passthrough off" if audio.input_thru_get_mute() else "passthrough on")
+        ctx.text("passthrough off" if self._mute else "passthrough on")
         ctx.restore()
 
         # bottom text
         ctx.move_to(0, 25)
         ctx.save()
         ctx.font_size = 15
-        ctx.text(STATE_TEXT.get(audio.input_get_source(), ""))
+        ctx.text(STATE_TEXT.get(self.target_source, ""))
 
-        # have red text when sleep mode isn't auto
-        if self._force_mode != "AUTO":
+        ctx.move_to(0, 40)
+        if self.source_connected:
+            # 0x3cb043, green
+            ctx.rgb(0.24, 0.69, 0.26)
+        else:
             # 0xff4500, red
             ctx.rgb(1, 0.41, 0)
-
-        ctx.move_to(0, 40)
-        ctx.text("(auto)" if self._force_mode == "AUTO" else "(forced)")
-
-        # mic has a loopback risk so has precautions
-        # so we warn users about it to not confuse them
-        if self._force_mode == "FORCE_MIC":
-            ctx.move_to(0, 55)
-            ctx.text("headphones only")
-            ctx.move_to(0, 70)
-            ctx.text("will not persist app exit")
+        if self._mute:
+            ctx.text("standby")
+        elif self._force_mode == "AUTO":
+            src = audio.input_thru_get_source()
+            if src != audio.INPUT_SOURCE_NONE:
+                ctx.text("connected to")
+                ctx.move_to(0, 56)
+                ctx.text(STATE_TEXT.get(src, ""))
+            else:
+                ctx.text("waiting...")
+        elif self._force_mode == "FORCE_MIC":
+            ctx.text("connected" if self.source_connected else "(headphones only)")
+        else:
+            ctx.text("connected" if self.source_connected else "waiting...")
         ctx.restore()
 
         # bottom button
@@ -94,48 +103,58 @@ class AudioPassthrough(Application):
         ctx.restore()
 
         ctx.move_to(0, 90)
-        ctx.text("force line in/out")
+        ctx.text("next source")
 
-    def on_exit(self) -> None:
-        # Mic passthrough has a loopback risk
-        if self._force_mode == "FORCE_MIC":
-            self._force_mode = "FORCE_NONE"
-            audio.input_set_source(audio.INPUT_SOURCE_NONE)
-            audio.input_thru_set_mute(True)
+    @property
+    def source_connected(self):
+        if self.source != audio.INPUT_SOURCE_NONE:
+            return self.source == audio.input_thru_get_source()
+        else:
+            return False
+
+    @property
+    def source(self):
+        if self._source is None:
+            self._source = audio.input_thru_get_source()
+        return self._source
+
+    @source.setter
+    def source(self, source):
+        audio.input_thru_set_source(source)
+        self._source = audio.input_thru_get_source()
 
     def think(self, ins: InputState, delta_ms: int) -> None:
         super().think(ins, delta_ms)
 
-        headset_connected = audio.headset_is_connected()
-        if self._force_mode == "FORCE_MIC":
-            audio.input_set_source(audio.INPUT_SOURCE_ONBOARD_MIC)
-        elif (
-            audio.line_in_is_connected() and self._force_mode == "AUTO"
-        ) or self._force_mode == "FORCE_LINE_IN":
-            audio.input_set_source(audio.INPUT_SOURCE_LINE_IN)
-        elif headset_connected or self._force_mode == "FORCE_LINE_OUT":
-            audio.input_set_source(audio.INPUT_SOURCE_HEADSET_MIC)
-        else:
-            audio.input_set_source(audio.INPUT_SOURCE_NONE)
-
         if ins.captouch.petals[0].pressed:
             if not self._button_0_pressed:
                 self._button_0_pressed = True
-                audio.input_thru_set_mute(not audio.input_thru_get_mute())
+                self._mute = not self._mute
         else:
             self._button_0_pressed = False
 
         if ins.captouch.petals[5].pressed:
             if not self._button_5_pressed:
                 self._button_5_pressed = True
-                self._force_mode = ForceModes[ForceModes.index(self._force_mode) + 1]
-                if ForceModes.index(self._force_mode) >= ForceModes.index("FORCE_NONE"):
-                    self._force_mode = "AUTO"
+                index = ForceModes.index(self._force_mode)
+                index = (index + 1) % 4
+                self._force_mode = ForceModes[index]
+
         else:
             self._button_5_pressed = False
 
-        if self._force_mode == "FORCE_MIC" and not audio.headphones_are_connected():
-            self._force_mode = "AUTO"
+        if self._mute:
+            self.source = audio.INPUT_SOURCE_NONE
+        else:
+            if self._force_mode == "FORCE_MIC":
+                self.target_source = audio.INPUT_SOURCE_ONBOARD_MIC
+            elif self._force_mode == "AUTO":
+                self.target_source = audio.INPUT_SOURCE_AUTO
+            elif self._force_mode == "FORCE_LINE_IN":
+                self.target_source = audio.INPUT_SOURCE_LINE_IN
+            elif self._force_mode == "FORCE_LINE_OUT":
+                self.target_source = audio.INPUT_SOURCE_HEADSET_MIC
+            self.source = self.target_source
 
 
 # For running with `mpremote run`:
diff --git a/python_payload/apps/tiny_sampler/__init__.py b/python_payload/apps/tiny_sampler/__init__.py
index eac6d1bcdaac7ebc3c959145323c283088484b5c..ae63c26338beee239a2bb531ed1be621a7c0018d 100644
--- a/python_payload/apps/tiny_sampler/__init__.py
+++ b/python_payload/apps/tiny_sampler/__init__.py
@@ -34,7 +34,6 @@ class TinySampler(Application):
             self.blm = bl00mbox.Channel("tiny sampler")
         self.samplers: List[bl00mbox.patches._Patch | Any] = [None] * 5
         self.line_in = self.blm.new(bl00mbox.plugins.bl00mbox_line_in)
-        self.line_in.signals.gain = 32000
         for i in range(5):
             self.samplers[i] = self.blm.new(bl00mbox.patches.sampler, 1000)
             self.samplers[i].signals.output = self.blm.mixer
@@ -208,6 +207,20 @@ class TinySampler(Application):
                 elif not ct.petals[i].pressed and self.ct_prev.petals[i].pressed:
                     self.release_event[i] = True
 
+        if self.mode == 0:
+            if self.orig_source == audio.INPUT_SOURCE_NONE:
+                if audio.input_engine_get_source_avail(audio.INPUT_SOURCE_ONBOARD_MIC):
+                    audio.input_engine_set_source(audio.INPUT_SOURCE_ONBOARD_MIC)
+                else:
+                    audio.input_engine_set_source(audio.INPUT_SOURCE_AUTO)
+            else:
+                if audio.input_engine_get_source_avail(self.orig_source):
+                    audio.input_engine_set_source(audio.self.orig_source)
+                else:
+                    audio.input_engine_set_source(audio.INPUT_SOURCE_AUTO)
+        else:
+            audio.input_engine_set_source(audio.INPUT_SOURCE_NONE)
+
         if self.mode == 0 or release_all:
             for i in range(5):
                 if not self.is_recording[i]:
@@ -268,18 +281,18 @@ class TinySampler(Application):
         self.ct_prev = ct
 
     def on_enter(self, vm) -> None:
-        self.mode = 0
         super().on_enter(vm)
-        audio.input_set_source(audio.INPUT_SOURCE_ONBOARD_MIC)
+        self.mode = 0
+        self.orig_source = audio.input_engine_get_source()
         if self.blm is None:
             self._build_synth()
 
     def on_exit(self) -> None:
+        audio.input_engine_set_source(self.orig_source)
         for i in range(5):
             if self.is_recording[i]:
                 self.samplers[i].signals.rec_trigger.stop()
                 self.is_recording[i] = False
-        audio.input_set_source(audio.INPUT_SOURCE_NONE)
         if self.blm is not None:
             self.blm.clear()
             self.blm.free = True
diff --git a/python_payload/st3m/run.py b/python_payload/st3m/run.py
index 9461fb10cd85be0041bfa3b36b7ed5629718ccfc..cad161730a285e4a95e88e09d15aa7a2bc7a3dae 100644
--- a/python_payload/st3m/run.py
+++ b/python_payload/st3m/run.py
@@ -186,6 +186,18 @@ def run_main() -> None:
     audio.speaker_set_minimum_volume_dB(settings.num_speaker_min_db.value)
     audio.headphones_set_maximum_volume_dB(settings.num_headphones_max_db.value)
     audio.speaker_set_maximum_volume_dB(settings.num_speaker_max_db.value)
+
+    audio.headset_mic_set_gain_dB(settings.num_headset_mic_gain_db.value)
+    audio.onboard_mic_set_gain_dB(settings.num_onboard_mic_gain_db.value)
+    audio.line_in_set_gain_dB(settings.num_line_in_gain_db.value)
+
+    audio.headset_mic_set_allowed(settings.onoff_headset_mic_allowed.value)
+    audio.onboard_mic_set_allowed(settings.onoff_onboard_mic_allowed.value)
+    audio.line_in_set_allowed(settings.onoff_line_in_allowed.value)
+    audio.onboard_mic_to_speaker_set_allowed(
+        settings.onoff_onboard_mic_to_speaker_allowed.value
+    )
+
     leds.set_brightness(settings.num_leds_brightness.value)
     sys_display.set_backlight(settings.num_display_brightness.value)
 
diff --git a/python_payload/st3m/settings.py b/python_payload/st3m/settings.py
index ddd70372de392db805ddc8cab87f6ff4c77b818c..554a353d32ec2b9d48389e648a36491205039bfe 100644
--- a/python_payload/st3m/settings.py
+++ b/python_payload/st3m/settings.py
@@ -241,9 +241,29 @@ num_speaker_max_db = StringTunable(
 )
 
 onoff_speaker_eq_on = StringTunable("Speaker EQ On", "system.audio.speaker_eq_on", True)
+onoff_headset_mic_allowed = StringTunable(
+    "Headset Mic Allowed", "system.audio.headset_mic_allowed", True
+)
+onoff_onboard_mic_allowed = StringTunable(
+    "Onboard Mic Allowed", "system.audio.onboard_mic_allowed", True
+)
+onoff_line_in_allowed = StringTunable(
+    "Line In Allowed", "system.audio.line_in_allowed", True
+)
+onoff_onboard_mic_to_speaker_allowed = StringTunable(
+    "Onboard Mic To Speaker Allowed",
+    "system.audio.onboard_mic_to_speaker_allowed",
+    False,
+)
 
-num_headset_gain_db = StringTunable(
-    "Headset Mic Gain dB", "system.audio.headset_gain_dB", 10
+num_headset_mic_gain_db = StringTunable(
+    "Headset Mic Gain dB", "system.audio.headset_mic_gain_dB", 0
+)
+num_onboard_mic_gain_db = StringTunable(
+    "Onboard Mic Gain dB", "system.audio.onboard_mic_gain_dB", 0
+)
+num_line_in_gain_db = StringTunable(
+    "Line In Gain dB", "system.audio.line_in_gain_dB", 0
 )
 
 num_display_brightness = StringTunable(
@@ -282,7 +302,13 @@ load_save_settings: List[UnaryTunable] = [
     num_headphones_max_db,
     num_speaker_max_db,
     onoff_speaker_eq_on,
-    num_headset_gain_db,
+    onoff_headset_mic_allowed,
+    onoff_onboard_mic_allowed,
+    onoff_line_in_allowed,
+    onoff_onboard_mic_to_speaker_allowed,
+    num_headset_mic_gain_db,
+    num_onboard_mic_gain_db,
+    num_line_in_gain_db,
     num_display_brightness,
     num_leds_brightness,
     num_leds_speed,
diff --git a/sim/fakes/audio.py b/sim/fakes/audio.py
index 23dfab45b36c72e99be45efb1c0e0340e1e53d6b..1bcae08891e46314834c1499a25f0b332702f310 100644
--- a/sim/fakes/audio.py
+++ b/sim/fakes/audio.py
@@ -71,9 +71,57 @@ def get_mute() -> bool:
     return _muted
 
 
-def headset_set_gain_dB(v: float) -> None:
+def headset_mic_set_gain_dB(v: float) -> None:
     pass
 
 
-def headset_get_gain_dB() -> float:
+def headset_mic_get_gain_dB() -> float:
     return 10
+
+
+def onboard_mic_set_gain_dB(v: float) -> None:
+    pass
+
+
+def onboard_mic_get_gain_dB() -> float:
+    return 10
+
+
+def line_in_set_gain_dB(v: float) -> None:
+    pass
+
+
+def line_in_get_gain_dB() -> float:
+    return 10
+
+
+def headset_mic_set_allowed(v: bool) -> None:
+    pass
+
+
+def onboard_mic_set_allowed(v: bool) -> None:
+    pass
+
+
+def line_in_set_allowed(v: bool) -> None:
+    pass
+
+
+def onboard_mic_to_speaker_set_allowed(v: bool) -> None:
+    pass
+
+
+def headget_mic_get_allowed() -> bool:
+    return True
+
+
+def onboard_mic_get_allowed() -> bool:
+    return True
+
+
+def line_in_get_allowed() -> bool:
+    return True
+
+
+def onboard_mic_to_speaker_get_allowed() -> bool:
+    return False