From c2f5a4350a110bb38ad25dc02b10ff4349fc1bbd Mon Sep 17 00:00:00 2001
From: moon2 <moon2protonmail@protonmail.com>
Date: Tue, 6 Jun 2023 19:28:54 +0000
Subject: [PATCH] Audio IO: Speaker/Headphone Management

(not properly exposed in python_payload yet)
---
 components/badge23/audio.c                 | 471 ++++++++++++++++-----
 components/badge23/captouch.c              |   5 +
 components/badge23/espan.c                 |  33 +-
 components/badge23/include/badge23/audio.h | 156 ++++++-
 components/badge23/include/badge23/lock.h  |   6 +
 components/badge23/spio.c                  |   5 +
 python_payload/demo_menu.py                |   3 +-
 python_payload/demo_sparabo.py             |   5 +-
 python_payload/menu_settings.py            |   5 +-
 python_payload/menu_tinysynth.py           |   5 +-
 python_payload/utils.py                    |   8 +-
 usermodule/micropython.cmake               |   1 +
 usermodule/mp_audio.c                      | 234 ++++++++++
 usermodule/mp_hardware.c                   |   7 +-
 14 files changed, 822 insertions(+), 122 deletions(-)
 create mode 100644 components/badge23/include/badge23/lock.h
 create mode 100644 usermodule/mp_audio.c

diff --git a/components/badge23/audio.c b/components/badge23/audio.c
index 76b130d511..f30dd972e8 100644
--- a/components/badge23/audio.c
+++ b/components/badge23/audio.c
@@ -1,12 +1,12 @@
 #include "badge23/audio.h"
 #include "badge23/synth.h" 
 #include "badge23/scope.h"
+#include "badge23/lock.h"
 #include "badge23_hwconfig.h"
 
 #include "driver/i2s.h"
 #include "driver/i2c.h"
 
-
 #include <freertos/FreeRTOS.h>
 #include <freertos/task.h>
 #include <freertos/queue.h>
@@ -14,103 +14,151 @@
 #include <math.h>
 #include <string.h>
 
-#define TIMEOUT_MS                  1000
+#define TIMEOUT_MS 1000
 
-#define I2C_MASTER_NUM              0                          /*!< I2C master i2c port number, the number of i2c peripheral interfaces available will depend on the chip */
+#define I2C_MASTER_NUM 0 /*!< I2C master i2c port number, the number of i2c peripheral interfaces available will depend on the chip */
 
 static void audio_player_task(void* arg);
 
-#define DMA_BUFFER_SIZE     64
-#define DMA_BUFFER_COUNT    2
+#define DMA_BUFFER_SIZE 64
+#define DMA_BUFFER_COUNT 2
 #define I2S_PORT 0
 
+// used for exp(vol_dB * NAT_LOG_DB)
+#define NAT_LOG_DB 0.1151292546497023
+
+// placeholder for "fake mute" -inf dB (we know floats can do that but we have trust issues when using NAN)
+#define SILLY_LOW_VOLUME_DB (-10000.)
+
+static bool headphones_connected = 0;
+static bool headset_connected = 0;
+static bool line_in_connected = 0;
+static bool headphones_detection_override = 0;
+static int32_t software_volume = 0;
+
+// maybe struct these someday but eh it works
+static float headphones_volume_dB = 0;
+static bool headphones_mute = 0;
+const static float headphones_maximum_volume_system_dB = 3;
+static float headphones_maximum_volume_user_dB = headphones_maximum_volume_system_dB;
+static float headphones_minimum_volume_user_dB = headphones_maximum_volume_system_dB - 70;
+
+static float speaker_volume_dB = 0;
+static bool speaker_mute = 0;
+const static float speaker_maximum_volume_system_dB = 14;
+static float speaker_maximum_volume_user_dB = speaker_maximum_volume_system_dB;
+static float speaker_minimum_volume_user_dB = speaker_maximum_volume_system_dB - 60;
+
+uint8_t audio_headset_is_connected(){ return headset_connected; }
+uint8_t audio_headphones_are_connected(){ return headphones_connected || headphones_detection_override; }
+float audio_headphones_get_volume_dB(){ return headphones_volume_dB; }
+float audio_speaker_get_volume_dB(){ return speaker_volume_dB; }
+float audio_headphones_get_minimum_volume_dB(){ return headphones_minimum_volume_user_dB; }
+float audio_speaker_get_minimum_volume_dB(){ return speaker_minimum_volume_user_dB; }
+float audio_headphones_get_maximum_volume_dB(){ return headphones_maximum_volume_user_dB; }
+float audio_speaker_get_maximum_volume_dB(){ return speaker_maximum_volume_user_dB; }
+uint8_t audio_headphones_get_mute(){ return headphones_mute ? 1 : 0; }
+uint8_t audio_speaker_get_mute(){ return speaker_mute ? 1 : 0; }
+
 #if defined(CONFIG_BADGE23_HW_GEN_P3) || defined(CONFIG_BADGE23_HW_GEN_P4)
+
 static uint8_t max98091_i2c_read(const uint8_t reg)
 {
     const uint8_t tx[] = {reg};
     uint8_t rx[1];
+    xSemaphoreTake(mutex_i2c, portMAX_DELAY);
     esp_err_t ret = i2c_master_write_read_device(I2C_MASTER_NUM, 0x10, tx, sizeof(tx), rx, sizeof(rx), TIMEOUT_MS / portTICK_PERIOD_MS);
+    xSemaphoreGive(mutex_i2c);
     return rx[0];
 }
 
 static esp_err_t max98091_i2c_write(const uint8_t reg, const uint8_t data)
 {
     const uint8_t tx[] = {reg, data};
+    xSemaphoreTake(mutex_i2c, portMAX_DELAY);
     esp_err_t ret = i2c_master_write_to_device(I2C_MASTER_NUM, 0x10, tx, sizeof(tx), TIMEOUT_MS / portTICK_PERIOD_MS);
-    if(max98091_i2c_read(reg) != data) printf("readback of %04X to %02X write failed\n", data, reg);
+    xSemaphoreGive(mutex_i2c);
     return ret;
 }
 
-
+static esp_err_t max98091_i2c_write_readback(const uint8_t reg, const uint8_t data)
+{
+    const uint8_t tx[] = {reg, data};
+    xSemaphoreTake(mutex_i2c, portMAX_DELAY);
+    esp_err_t ret = i2c_master_write_to_device(I2C_MASTER_NUM, 0x10, tx, sizeof(tx), TIMEOUT_MS / portTICK_PERIOD_MS);
+    xSemaphoreGive(mutex_i2c);
+    if(max98091_i2c_read(reg) != data) printf("readback of %04X to %02X write failed\n", data, reg);
+    return ret;
+}
 
 static void init_codec()
 {
     // Enable CODEC
 
     vTaskDelay(10 / portTICK_PERIOD_MS);
-    ESP_ERROR_CHECK(max98091_i2c_write(0x00, 0x80)); // shutdown
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x00, 0x80)); // shutdown
     vTaskDelay(10 / portTICK_PERIOD_MS);
 
-    ESP_ERROR_CHECK(max98091_i2c_write(0x45, 0)); // shutdown
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x45, 0)); // shutdown
 
-    ESP_ERROR_CHECK(max98091_i2c_write(0x1b, 1 << 4)); // pclk = mclk / 1
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x1b, 1 << 4)); // pclk = mclk / 1
 
-    ESP_ERROR_CHECK(max98091_i2c_write(0x26,  (1 << 7) | (1 << 6))); // music, dc filter in record
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x26,  (1 << 7) | (1 << 6))); // music, dc filter in record
 
-    ESP_ERROR_CHECK(max98091_i2c_write(0x06, 1 << 2)); // Sets up DAI for left-justified slave mode operation.
-    ESP_ERROR_CHECK(max98091_i2c_write(0x07, 1 << 5)); // Sets up the DAC to speaker path
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x06, 1 << 2)); // Sets up DAI for left-justified slave mode operation.
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x07, 1 << 5)); // Sets up the DAC to speaker path
 
     // Somehow this was needed to get an input signal to the ADC, even though
     // all other registers should be taken care of later. Don't know why.
-    ESP_ERROR_CHECK(max98091_i2c_write(0x09, 1 << 6)); // Sets up the line in to adc path
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x09, 1 << 6)); // Sets up the line in to adc path
 
-    ESP_ERROR_CHECK(max98091_i2c_write(0x25, (1 << 1) | (1 << 0))); // SDOUT, SDIN enabled
-    ESP_ERROR_CHECK(max98091_i2c_write(0x42, 1 << 0)); // bandgap bias
-    ESP_ERROR_CHECK(max98091_i2c_write(0x43, 1 << 0)); // high performane mode
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x25, (1 << 1) | (1 << 0))); // SDOUT, SDIN enabled
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x42, 1 << 0)); // bandgap bias
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x43, 1 << 0)); // high performane mode
 
     // Table 51. Digital Audio Interface (DAI) Format Configuration Register
 
-    ESP_ERROR_CHECK(max98091_i2c_write(0x2E, 1)); // Left DAC -> Left Speaker
-    ESP_ERROR_CHECK(max98091_i2c_write(0x2F, 2)); // Right DAC -> Right Speaker
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x2E, 1)); // Left DAC -> Left Speaker
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x2F, 2)); // Right DAC -> Right Speaker
 
-    //max98091_i2c_write(0x2E, (1<<2) | (1<<1)); // Line A -> Left Speaker
-    //max98091_i2c_write(0x2F, (1<<3) | (1<<0)); // LIne B -> Right Speaker
+    //max98091_i2c_write_readback(0x2E, (1<<2) | (1<<1)); // Line A -> Left Speaker
+    //max98091_i2c_write_readback(0x2F, (1<<3) | (1<<0)); // LIne B -> Right Speaker
 
-    ESP_ERROR_CHECK(max98091_i2c_write(0x29, 1)); // Left DAC -> Left HP
-    ESP_ERROR_CHECK(max98091_i2c_write(0x2A, 2)); // Right DAC -> Right HP
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x29, 1)); // Left DAC -> Left HP
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x2A, 2)); // Right DAC -> Right HP
 
     // Mic bias is off
-    ESP_ERROR_CHECK(max98091_i2c_write(0x3E, (1<<4) |(1<<3) | (1<<2) | (1<<1) | (1<<0))); // enable micbias, line input amps, ADCs
-    ESP_ERROR_CHECK(max98091_i2c_write(0x0D, (1<<3) | (1<<2))); // IN3 SE -> Line A, IN4 SE -> Line B
-    ESP_ERROR_CHECK(max98091_i2c_write(0x15, (1<<4) )); // line B -> left ADC
-    ESP_ERROR_CHECK(max98091_i2c_write(0x16, (1<<3) )); // line A -> right ADC
-    ESP_ERROR_CHECK(max98091_i2c_write(0x44, (1<<2) | (1<<1) | (1<<0) )); // 128x oversampling, dithering, high performance ADC
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x3E, (1<<4) |(1<<3) | (1<<2) | (1<<1) | (1<<0))); // enable micbias, line input amps, ADCs
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x0D, (1<<3) | (1<<2))); // IN3 SE -> Line A, IN4 SE -> Line B
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x15, (1<<4) )); // line B -> left ADC
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x16, (1<<3) )); // line A -> right ADC
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x44, (1<<2) | (1<<1) | (1<<0) )); // 128x oversampling, dithering, high performance ADC
 
-    max98091_i2c_write(0x13, (1<<4) | (1<<5) | (1<<1) | (1<<0) ); // enable digital mic
+    max98091_i2c_write_readback(0x13, (1<<4) | (1<<5) | (1<<1) | (1<<0) ); // enable digital mic
 
     // Enable headset mic
 #if 0
-    max98091_i2c_write(0x13, 0);
-    ESP_ERROR_CHECK(max98091_i2c_write(0x0F, (0<<1) | (1<<0) )); // IN5/IN6 to MIC1
-    ESP_ERROR_CHECK(max98091_i2c_write(0x10, (1<<6) | (1<<4) | (1<<2) )); // 20 dB gain on MIC1
-    ESP_ERROR_CHECK(max98091_i2c_write(0x15, (1<<5) )); // MIC1 -> left ADC
-    ESP_ERROR_CHECK(max98091_i2c_write(0x16, (1<<5) )); // MIC1 -> right ADC
+    max98091_i2c_write_readback(0x13, 0);
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x0F, (0<<1) | (1<<0) )); // IN5/IN6 to MIC1
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x10, (1<<6) | (1<<4) | (1<<2) )); // 20 dB gain on MIC1
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x15, (1<<5) )); // MIC1 -> left ADC
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x16, (1<<5) )); // MIC1 -> right ADC
 #endif
 
-    ESP_ERROR_CHECK(max98091_i2c_write(0x3F, (1<<1) | (1<<0))); // output enable: enable dacs
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x3F, (1<<1) | (1<<0))); // output enable: enable dacs
 
-    ESP_ERROR_CHECK(max98091_i2c_write(0x45, 1<<7)); // power up
-    //max98091_i2c_write(0x31, 0x2c); // 0db, no mute
-    //max98091_i2c_write(0x32, 0x2c); // 0db, no mute
-    ESP_ERROR_CHECK(max98091_i2c_write(0x3F, (1<<7) | (1<<6) | (1<<5) | (1<<4) | (1<<1) | (1<<0))); // enable outputs, dacs
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x45, 1<<7)); // power up
+    //max98091_i2c_write_readback(0x31, 0x2c); // 0db, no mute
+    //max98091_i2c_write_readback(0x32, 0x2c); // 0db, no mute
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x3F, (1<<7) | (1<<6) | (1<<5) | (1<<4) | (1<<1) | (1<<0))); // enable outputs, dacs
 
-    //max98091_i2c_write(0x27, (1<<4) | (1<<5)); // full playback gain
+    //max98091_i2c_write_readback(0x27, (1<<4) | (1<<5)); // full playback gain
 
-    //max98091_i2c_write(0x31, 0x3f); // +14 db speaker
-    //max98091_i2c_write(0x32, 0x3f); // +14 db speaker
-    ESP_ERROR_CHECK(max98091_i2c_write(0x41, 0x0));
+    //max98091_i2c_write_readback(0x31, 0x3f); // +14 db speaker
+    //max98091_i2c_write_readback(0x32, 0x3f); // +14 db speaker
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x41, 0x0));
 
-    ESP_ERROR_CHECK(max98091_i2c_write(0x3D, 1<<7)); // jack detect enable
+    ESP_ERROR_CHECK(max98091_i2c_write_readback(0x3D, 1<<7)); // jack detect enable
     printf("4 readbacks failing here is normal dw ^w^");
 }
 
@@ -144,8 +192,78 @@ static void i2s_init(void){
 
 }
 
+// irregular register value -> vol mapping taken from codec datasheet
+typedef struct {
+    uint8_t register_value;
+    float volume_dB;
+} vol_map_t;
+
+const uint8_t speaker_map_len = 40;
+const vol_map_t speaker_map[] = {{0x3F, +14}, {0x3E, +13.5}, {0x3D, +13}, {0x3C, +12.5}, {0x3B, +12}, {0x3A, +11.5}, {0x39, +11}, {0x38, +10.5}, {0x37, +10}, {0x36, +9.5}, {0x35, +9}, {0x34, +8}, {0x33, +7}, {0x32, +6}, {0x31, +5}, {0x30, +4}, {0x2F, +3}, {0x2E, +2}, {0x2D, +1}, {0x2C, +0}, {0x2B, -1}, {0x2A, -2}, {0x29, -3}, {0x28, -4}, {0x27, -5}, {0x26, -6}, {0x25, -8}, {0x24, -10}, {0x23, -12}, {0x22, -14}, {0x21, -17}, {0x20, -20}, {0x1F, -23}, {0x1E, -26}, {0x1D, -29}, {0x1C, -32}, {0x1B, -36}, {0x1A, -40}, {0x19, -44}, {0x18, -48}};
+
+const uint8_t headphones_map_len = 32;
+const vol_map_t headphones_map[] = {{0x1F, +3}, {0x1E, +2.5}, {0x1D, +2}, {0x1C, +1.5}, {0x1B, +1}, {0x1A, +0}, {0x19, -1}, {0x18, -2}, {0x17, -3}, {0x16, -4}, {0x15, -5}, {0x14, -7}, {0x13, -9}, {0x12, -11}, {0x11, -13}, {0x10, -15}, {0x0F, -17}, {0x0E, -19}, {0x0D, -22}, {0x0C, -25}, {0x0B, -28}, {0x0A, -31}, {0x09, -34}, {0x08, -37}, {0x07, -40}, {0x06, -43}, {0x06, -47}, {0x04, -51}, {0x03, -55}, {0x02, -59}, {0x01, -63}, {0x00, -67}};
+
+void _audio_headphones_set_volume_dB(float vol_dB, bool mute){
+    uint8_t map_index = headphones_map_len - 1;
+    for(; map_index; map_index--){
+        if(headphones_map[map_index].volume_dB >= vol_dB) break; 
+    }
+    uint8_t reg = headphones_map[map_index].register_value;
+    reg = (mute ? (1 << 7) : 0) | reg;
+    max98091_i2c_write(0x2C, reg); //left chan
+    max98091_i2c_write(0x2D, reg); //right chan
+    // note: didn't check if chan physically mapped to l/r or flipped.
+
+    // do the fine steps in software
+    // note: synchronizing both hw and software volume changes is somewhat tricky
+    float hardware_volume_dB = headphones_map[map_index].volume_dB;
+    float software_volume_dB = vol_dB - hardware_volume_dB;
+    if(software_volume_dB > 0) software_volume_dB = 0;
+    //if(!software_volume_enabled) software_volume_dB = 0; // breaks p1, might add option once it is retired
+    software_volume = (int32_t) (32768 * exp(software_volume_dB * NAT_LOG_DB));
+    headphones_volume_dB = hardware_volume_dB + software_volume_dB;
+}
+
+void _audio_speaker_set_volume_dB(float vol_dB, bool mute){
+    uint8_t map_index = speaker_map_len - 1;
+    for(; map_index; map_index--){
+        if(speaker_map[map_index].volume_dB >= vol_dB) break; 
+    }
+
+    uint8_t reg = speaker_map[map_index].register_value;
+    reg = (mute ?  (1 << 7) : 0) | reg;
+    max98091_i2c_write(0x31, reg); //left chan
+    max98091_i2c_write(0x32, reg); //right chan
+    //note: didn't check if chan physically mapped to l/r or flipped.
+
+    // do the fine steps in software
+    // note: synchronizing both hw and software volume changes is somewhat tricky
+    float hardware_volume_dB = speaker_map[map_index].volume_dB;
+    float software_volume_dB = vol_dB - hardware_volume_dB;
+    if(software_volume_dB > 0) software_volume_dB = 0;
+    //if(!software_volume_enabled) software_volume_dB = 0; // breaks p1, might add option once it is retired
+    software_volume = (int32_t) (32768. * exp(software_volume_dB * NAT_LOG_DB));
+    speaker_volume_dB = hardware_volume_dB + software_volume_dB;
+}
+
+void audio_headphones_set_mute(uint8_t mute){
+    headphones_mute = mute;
+    audio_headphones_set_volume_dB(headphones_volume_dB);
+}
+
+void audio_speaker_set_mute(uint8_t mute){
+    speaker_mute = mute;
+    audio_speaker_set_volume_dB(speaker_volume_dB);
+}
+
 #elif defined(CONFIG_BADGE23_HW_GEN_P1)
 
+#define MAX_VOLUME_DB 10
+#define MIN_VOLUME_DB (-80)
+
+int32_t software_volume_premute; // ugly but this is an old prototype that will be phased out soon
+
 static void i2s_init(void){
     
     static const i2s_config_t i2s_config = {
@@ -172,10 +290,219 @@ static void i2s_init(void){
 
     i2s_set_pin(I2S_PORT, &pin_config);
 }
+
+void _audio_speaker_set_volume_dB(float vol_dB, bool mute){
+    int32_t buf =  32767 * exp(vol_dB * NAT_LOG_DB);
+    software_volume_premute = buf;
+    if(mute){
+        software_volume = 0;
+    } else {
+        software_volume = software_volume_premute;
+    }
+    speaker_volume_dB = vol_dB;
+}
+
+void _audio_headphones_set_volume_dB(float vol_dB, bool mute){
+}
+
+void audio_headphones_set_mute(uint8_t mute){
+    headphones_mute = 1;
+};
+
+void audio_speaker_set_mute(uint8_t mute){
+    speaker_mute = mute;
+    if(speaker_mute){
+        software_volume = 0;
+    } else {
+        software_volume = software_volume_premute;
+    }
+}
+
 #else
 #error "audio not implemented for this badge generation"
 #endif
 
+float audio_speaker_set_volume_dB(float vol_dB){
+    bool mute  = speaker_mute || audio_headphones_are_connected();
+    if(vol_dB > speaker_maximum_volume_user_dB) vol_dB = speaker_maximum_volume_user_dB;
+    if(vol_dB < speaker_minimum_volume_user_dB){
+        vol_dB = SILLY_LOW_VOLUME_DB; // fake mute
+        mute = 1;
+    }
+    _audio_speaker_set_volume_dB(vol_dB, mute);
+    return speaker_volume_dB;
+}
+
+float audio_headphones_set_volume_dB(float vol_dB){
+    bool mute  = headphones_mute || (!audio_headphones_are_connected());
+    if(vol_dB > headphones_maximum_volume_user_dB) vol_dB = headphones_maximum_volume_user_dB;
+    if(vol_dB < headphones_minimum_volume_user_dB){
+        vol_dB = SILLY_LOW_VOLUME_DB; // fake mute
+        mute = 1;
+    }
+    _audio_headphones_set_volume_dB(vol_dB, mute);
+    return headphones_volume_dB;
+}
+
+void audio_headphones_detection_override(uint8_t enable){
+    headphones_detection_override = enable;
+    audio_headphones_set_volume_dB(headphones_volume_dB);
+    audio_speaker_set_volume_dB(speaker_volume_dB);
+}
+
+float audio_headphones_adjust_volume_dB(float vol_dB){
+    if(audio_headphones_get_volume_dB() < headphones_minimum_volume_user_dB){ //fake mute
+        if(vol_dB > 0){
+            return audio_headphones_set_volume_dB(headphones_minimum_volume_user_dB);
+        } else {
+            return audio_headphones_get_volume_dB();
+        }
+    } else { 
+        return audio_headphones_set_volume_dB(headphones_volume_dB + vol_dB);
+    }
+}
+
+float audio_speaker_adjust_volume_dB(float vol_dB){
+    if(audio_speaker_get_volume_dB() < speaker_minimum_volume_user_dB){ //fake mute
+        if(vol_dB > 0){
+            return audio_speaker_set_volume_dB(speaker_minimum_volume_user_dB);
+            printf("hi");
+        } else {
+            return audio_speaker_get_volume_dB();
+        }
+    } else { 
+        return audio_speaker_set_volume_dB(speaker_volume_dB + vol_dB);
+    }
+}
+
+float audio_adjust_volume_dB(float vol_dB){
+    if(audio_headphones_are_connected()){
+        return audio_headphones_adjust_volume_dB(vol_dB);
+    } else {
+        return audio_speaker_adjust_volume_dB(vol_dB);
+    }
+}
+
+float audio_set_volume_dB(float vol_dB){
+    if(audio_headphones_are_connected()){
+        return audio_headphones_set_volume_dB(vol_dB);
+    } else {
+        return audio_speaker_set_volume_dB(vol_dB);
+    }
+}
+
+float audio_get_volume_dB(){
+    if(audio_headphones_are_connected()){
+        return audio_headphones_get_volume_dB();
+    } else {
+        return audio_speaker_get_volume_dB();
+    }
+}
+
+void audio_set_mute(uint8_t mute){
+    if(audio_headphones_are_connected()){
+        audio_headphones_set_mute(mute);
+    } else {
+        audio_speaker_set_mute(mute);
+    }
+}
+
+uint8_t audio_get_mute(){
+    if(audio_headphones_are_connected()){
+        return audio_headphones_get_mute();
+    } else {
+        return audio_speaker_get_mute();
+    }
+}
+
+float audio_headphones_set_maximum_volume_dB(float vol_dB){
+    if(vol_dB > headphones_maximum_volume_system_dB) vol_dB = headphones_maximum_volume_system_dB;
+    if(vol_dB < headphones_minimum_volume_user_dB) vol_dB = headphones_minimum_volume_user_dB;
+    headphones_maximum_volume_user_dB = vol_dB;
+    return headphones_maximum_volume_user_dB;
+}
+
+float audio_headphones_set_minimum_volume_dB(float vol_dB){
+    if(vol_dB > headphones_maximum_volume_user_dB) vol_dB = headphones_maximum_volume_user_dB;
+    if((vol_dB + 1) < SILLY_LOW_VOLUME_DB) vol_dB = SILLY_LOW_VOLUME_DB + 1.;
+    headphones_minimum_volume_user_dB = vol_dB;
+    return headphones_minimum_volume_user_dB;
+}
+
+float audio_speaker_set_maximum_volume_dB(float vol_dB){
+    if(vol_dB > speaker_maximum_volume_system_dB) vol_dB = speaker_maximum_volume_system_dB;
+    if(vol_dB < speaker_minimum_volume_user_dB) vol_dB = speaker_minimum_volume_user_dB;
+    speaker_maximum_volume_user_dB = vol_dB;
+    return speaker_maximum_volume_user_dB;
+}
+
+float audio_speaker_set_minimum_volume_dB(float vol_dB){
+    if(vol_dB > speaker_maximum_volume_user_dB) vol_dB = speaker_maximum_volume_user_dB;
+    if((vol_dB + 1) < SILLY_LOW_VOLUME_DB) vol_dB = SILLY_LOW_VOLUME_DB + 1.;
+    speaker_minimum_volume_user_dB = vol_dB;
+    return speaker_minimum_volume_user_dB;
+}
+
+float audio_speaker_get_volume_relative(){
+    float ret = audio_speaker_get_volume_dB();
+    if(ret < speaker_minimum_volume_user_dB) return 0; // fake mute
+    float vol_range = speaker_maximum_volume_user_dB - speaker_minimum_volume_user_dB;
+    ret -= speaker_minimum_volume_user_dB; // shift to above zero
+    ret /= vol_range; // restrict to 0..1 range
+    return (ret*0.99) + 0.01; // shift to 0.01 to 0.99 range to distinguish from fake mute
+}
+
+float audio_headphones_get_volume_relative(){
+    float ret = audio_headphones_get_volume_dB();
+    if(ret < headphones_minimum_volume_user_dB) return 0; // fake mute
+    float vol_range = headphones_maximum_volume_user_dB - headphones_minimum_volume_user_dB;
+    ret -= headphones_minimum_volume_user_dB; // shift to above zero
+    ret /= vol_range; // restrict to 0..1 range
+    return (ret*0.99) + 0.01; // shift to 0.01 to 0.99 range to distinguish from fake mute
+}
+
+float audio_get_volume_relative(){
+    if(audio_headphones_are_connected()){
+        return audio_headphones_get_volume_relative();
+    } else {
+        return audio_speaker_get_volume_relative();
+    }
+}
+
+void audio_update_jacksense(){
+#if defined(CONFIG_BADGE23_HW_GEN_P1)
+    line_in_connected = 0;
+#elif defined(CONFIG_BADGE23_HW_GEN_P3) || defined(CONFIG_BADGE23_HW_GEN_P4)
+    line_in_connected = 1;
+#elif defined(CONFIG_BADGE23_HW_GEN_P6)
+    line_in_connected = 0; //TODO: read port expander
+#endif
+
+#if defined(CONFIG_BADGE23_HW_GEN_P1)
+    headphones_connected = 0;
+    headset_connected = 0;
+#elif defined(CONFIG_BADGE23_HW_GEN_P3) || defined(CONFIG_BADGE23_HW_GEN_P4)  || defined(CONFIG_BADGE23_HW_GEN_P6)
+    static uint8_t jck_prev = 255; // unreachable value -> initial comparision always untrue
+    uint8_t jck = max98091_i2c_read(0x02);
+    if(jck == 6){
+        headphones_connected = 0;
+        headset_connected = 0;
+    } else if(jck == 0){
+        headphones_connected = 1;
+        headset_connected = 0;
+    } else if(jck == 2){
+        headphones_connected = 1;
+        headset_connected = 1;
+    }
+
+    if(jck != jck_prev){ // update volume to trigger mutes if needed
+        audio_speaker_set_volume_dB(speaker_volume_dB);
+        audio_headphones_set_volume_dB(headphones_volume_dB);
+    }
+    jck_prev = jck;
+#endif
+}
+
 typedef struct _audio_source_t{
     void * render_data;
     float (* render_function)(void *);
@@ -258,29 +585,15 @@ uint16_t count_audio_sources(){
 }
 
 static void _audio_init(void) {
+    // TODO: this assumes I2C is already initialized
     init_scope(241);
     i2s_init();
+    audio_update_jacksense();
     //ESP_ERROR_CHECK(i2s_channel_enable(tx_chan));
     TaskHandle_t handle;
     xTaskCreate(&audio_player_task, "Audio player", 3000, NULL, configMAX_PRIORITIES - 1, &handle);
 }
 
-#define LR_PHASE 1
-#define NAT_LOG_DB 0.1151292546497023
-
-static uint16_t _global_vol = 3000;
-
-void set_global_vol_dB(int8_t vol_dB){
-    if(vol_dB < (BADGE_MIN_VOLUME_DB)){
-        _global_vol = 0;
-    } else {
-        if(vol_dB > (BADGE_MAX_VOLUME_DB)) vol_dB = (BADGE_MAX_VOLUME_DB);
-        uint16_t buf =  3000 * exp(vol_dB * NAT_LOG_DB);
-        if(buf > (BADGE_VOLUME_LIMIT)) buf = (BADGE_VOLUME_LIMIT);
-        _global_vol = buf;
-    }
-}
-
 static void audio_player_task(void* arg) {
     int16_t buffer[DMA_BUFFER_SIZE * 2];
     memset(buffer, 0, sizeof(buffer));
@@ -295,11 +608,11 @@ static void audio_player_task(void* arg) {
                 audio_source = audio_source->next;
             }
             write_to_scope((int16_t) (1600. * sample));
-            sample = _global_vol * sample;
+            sample = software_volume * (sample/10);
             if(sample > 32767) sample = 32767;
             if(sample < -32767) sample = -32767;
             buffer[i] = (int16_t) sample;
-            buffer[i+1] = LR_PHASE * buffer[i];
+            buffer[i+1] = buffer[i];
         }
 
         size_t count = 0;
@@ -312,37 +625,3 @@ static void audio_player_task(void* arg) {
 }
 
 void audio_init() { _audio_init(); }
-
-
-/*
-#define NAT_LOG_SEMITONE 0.05776226504666215
-
-void synth_set_bend(int i, float bend){
-    if(bend != bend) return;
-    if((bend > -0.0001) && (bend < 0.0001)){
-        synths[i].bend = 1;
-    } else {
-        synths[i].bend = exp(bend * NAT_LOG_SEMITONE);
-    }
-}
-*/
-
-/*
-void synth_stop(int i){
-    if(synths[i].env_phase){
-        synths[i].env_phase = 3;
-    }
-}
-
-void synth_fullstop(int i){
-    synths[i].env_phase = 0;
-}
-
-void synth_start(int i){
-    synths[i].env_phase = 1; //put into attack phase;
-}
-
-float synth_get_env(int i){
-    return synths[i].env;
-}
-*/
diff --git a/components/badge23/captouch.c b/components/badge23/captouch.c
index 96082e60ac..8916c9269b 100644
--- a/components/badge23/captouch.c
+++ b/components/badge23/captouch.c
@@ -6,6 +6,7 @@
 #include <stdint.h>
 #include <freertos/FreeRTOS.h>
 #include <freertos/atomic.h>
+#include "badge23/lock.h"
 
 #define PETAL_PAD_TIP 0
 #define PETAL_PAD_CCW 1
@@ -102,7 +103,9 @@ static struct ad714x_chip chip_bot_rev5 = {.addr = AD7147_ADDR_BOT, .gpio = 15,
 static esp_err_t ad714x_i2c_write(const struct ad714x_chip *chip, const uint16_t reg, const uint16_t data)
 {
     const uint8_t tx[] = {reg >> 8, reg & 0xFF, data >> 8, data & 0xFF};
+    xSemaphoreTake(mutex_i2c, portMAX_DELAY);
     ESP_LOGI(TAG, "AD7147 write reg %X-> %X", reg, data);
+    xSemaphoreGive(mutex_i2c);
     return i2c_master_write_to_device(I2C_MASTER_NUM, chip->addr, tx, sizeof(tx), TIMEOUT_MS / portTICK_PERIOD_MS);
 }
 
@@ -110,7 +113,9 @@ static esp_err_t ad714x_i2c_read(const struct ad714x_chip *chip, const uint16_t
 {
     const uint8_t tx[] = {reg >> 8, reg & 0xFF};
     uint8_t rx[len * 2];
+    xSemaphoreTake(mutex_i2c, portMAX_DELAY);
     esp_err_t ret = i2c_master_write_read_device(I2C_MASTER_NUM, chip->addr, tx, sizeof(tx), rx, sizeof(rx), TIMEOUT_MS / portTICK_PERIOD_MS);
+    xSemaphoreGive(mutex_i2c);
     for(int i = 0; i < len; i++) {
         data[i] = (rx[i * 2] << 8) | rx[i * 2 + 1];
     }
diff --git a/components/badge23/espan.c b/components/badge23/espan.c
index 4df670e72a..0375ab5b45 100644
--- a/components/badge23/espan.c
+++ b/components/badge23/espan.c
@@ -4,6 +4,7 @@
 #include "badge23/display.h"
 #include "badge23/spio.h"
 #include "badge23_hwconfig.h"
+#include "badge23/lock.h"
 
 #include "esp_log.h"
 #include "driver/i2c.h"
@@ -35,6 +36,7 @@ static const char *TAG = "espan";
 #endif
 
 static QueueHandle_t i2c_queue = NULL;
+static QueueHandle_t slow_system_status_queue = NULL;
 static uint8_t dummy_data;
 
 static esp_err_t i2c_master_init(void)
@@ -56,6 +58,7 @@ static esp_err_t i2c_master_init(void)
 }
 
 #define CAPTOUCH_POLLING_PERIOD 10
+#define SLOW_SYSTEM_STATUS_PERIOD 200
 static uint8_t hw_init_done = 0;
 
 void i2c_timer(TimerHandle_t data){
@@ -70,20 +73,35 @@ void i2c_task(void * data){
     }
 }
 
+void slow_system_status_timer(TimerHandle_t data){
+    xQueueSend(slow_system_status_queue, &dummy_data, 0);
+}
+
+void slow_system_status_task(void * data){
+    while(1){
+        xQueueReceive(slow_system_status_queue, &dummy_data, portMAX_DELAY);
+        //read out stuff like jack detection, battery status, usb connection etc.
+        audio_update_jacksense();
+    }
+}
+
+void locks_init(){
+    mutex_i2c = xSemaphoreCreateMutex();
+}
+
 void os_app_main(void)
 {
+    locks_init();
     ESP_LOGI(TAG, "Starting on %s...", badge23_hw_name);
     ESP_ERROR_CHECK(i2c_master_init());
     ESP_LOGI(TAG, "I2C initialized successfully");
 
-    set_global_vol_dB(-90);
     audio_init();
     leds_init();
     init_buttons();
     captouch_init();
 
     //vTaskDelay(2000 / portTICK_PERIOD_MS);
-    set_global_vol_dB(0);
     captouch_force_calibration();
 
     display_init();
@@ -91,12 +109,19 @@ void os_app_main(void)
     i2c_queue = xQueueCreate(1,1);
 
     TaskHandle_t i2c_task_handle;
-    //xTaskCreate(&i2c_task, "I2C task", 4096, NULL, configMAX_PRIORITIES , &i2c_task_handle);
     xTaskCreatePinnedToCore(&i2c_task, "I2C task", 4096, NULL, configMAX_PRIORITIES-1, &i2c_task_handle, 0);
 
-
     TimerHandle_t i2c_timer_handle = xTimerCreate("I2C timer", pdMS_TO_TICKS(CAPTOUCH_POLLING_PERIOD), pdTRUE, (void *) 0, *i2c_timer);
     if( xTimerStart(i2c_timer_handle, 0 ) != pdPASS) ESP_LOGI(TAG, "I2C timer initialization failed");
+
+    slow_system_status_queue = xQueueCreate(1,1);
+
+    TaskHandle_t slow_system_status_task_handle;
+    xTaskCreatePinnedToCore(&slow_system_status_task, "slow system status task", 4096, NULL, configMAX_PRIORITIES-2, &slow_system_status_task_handle, 0);
+
+    TimerHandle_t slow_system_status_timer_handle = xTimerCreate("slow system status timer", pdMS_TO_TICKS(SLOW_SYSTEM_STATUS_PERIOD), pdTRUE, (void *) 0, *slow_system_status_timer);
+    if( xTimerStart(slow_system_status_timer_handle, 0 ) != pdPASS) ESP_LOGI(TAG, "I2C task initialization failed");
+
     hw_init_done = 1;
 }
 
diff --git a/components/badge23/include/badge23/audio.h b/components/badge23/include/badge23/audio.h
index 8de1ee6c5a..813a782bea 100644
--- a/components/badge23/include/badge23/audio.h
+++ b/components/badge23/include/badge23/audio.h
@@ -2,14 +2,160 @@
 #include <stdint.h>
 
 #define SAMPLE_RATE 16000
-#define BADGE_MAX_VOLUME_DB 20
-#define BADGE_MIN_VOLUME_DB (-80)
-#define BADGE_VOLUME_LIMIT 30000
 
+/* Initializes I2S bus, the audio task and required data structures.
+ * Expects an initialized I2C bus, will fail ungracefully otherwise (TODO).
+ * Requires the I2C lock.
+ */
 void audio_init();
 
-void set_global_vol_dB(int8_t vol_dB);
 uint16_t count_audio_sources();
-
 uint16_t add_audio_source(void * render_data, void * render_function);
 void remove_audio_source(uint16_t index);
+
+/* Polls hardware to check if headphones, headset or line in are plugged
+ * into the 3.5mm jacks. If it detects a plug in the headphone jack, speakers
+ * are automatically muted. There is no override for this at this moment.
+ * 
+ * Should be called periodically (100ms ish?) by a low priority task. Requires
+ * the I2C lock.
+ */
+void audio_update_jacksense(void);
+
+/* Returns 1 if headphones with or without microphone were connected to the
+ * headphone jack at the last call of audio_update_jacksense.
+ */
+uint8_t audio_headphones_are_connected();
+
+/* Returns 1 if headphones with microphone were connected to the headphone jack
+ * at the last call of audio_update_jacksense.
+ */
+uint8_t audio_headset_is_connected();
+
+/* If a sleeve contact mic doesn't pull the detection pin low enough the
+ * codec's built in headphone detection might fail. Calling this function
+ * with 'enable = 1' overrides the detection and assumes there's headphones
+ * plugged in. Call with 'enable = 0' to revert to automatic detection.
+ */
+void audio_headphones_detection_override(uint8_t enable);
+
+/* Attempts to set target volume for the headphone output/onboard speakers
+ * respectively, clamps/rounds if necessary and returns the actual volume.
+ * Absolute reference arbitrary.
+ * Does not unmute, use audio_{headphones_/speaker_/}set_mute as needed.
+ * Enters fake mute if requested volume is below the value set by
+ * audio_{headphones/speaker}_set_minimum_volume_user.
+ *
+ * Note: This function uses a hardware PGA for the coarse value and software
+ * for the fine value. These two methods are as of yet not synced so that there
+ * may be a transient volume "hiccup". "p1" badges only use software volume.
+ * The unspecified variant automatically chooses the adequate channel (**).
+ */
+float audio_headphones_set_volume_dB(float vol_dB);
+float audio_speaker_set_volume_dB(float vol_dB);
+float audio_set_volume_dB(float vol_dB);
+
+/* Like the audio_{headphones_/speaker_/}set_volume family but changes relative
+ * to last volume value.
+ */
+float audio_headphones_adjust_volume_dB(float vol_dB);
+float audio_speaker_adjust_volume_dB(float vol_dB);
+float audio_adjust_volume_dB(float vol_dB);
+
+/* Returns volume as set with audio_{headphones/speaker}_set_volume_dB.
+ * The unspecified variant automatically chooses the adequate channel (**).
+ */
+float audio_headphones_get_volume_dB();
+float audio_speaker_get_volume_dB();
+float audio_get_volume_dB();
+
+/* Mutes (mute = 1) or unmutes (mute = 0) the specified channel.
+ * The unspecified variant automatically chooses the adequate channel (**).
+ *
+ * Note: Even if a channel is unmuted it might not play sound depending on
+ * the return value of audio_headphone_are_connected. There is no override for
+ * this (see HEADPHONE PORT POLICY below).
+ */
+void audio_headphones_set_mute(uint8_t mute);
+void audio_speaker_set_mute(uint8_t mute);
+void audio_set_mute(uint8_t mute);
+
+/* Returns 1 if channel is muted, 0 if channel is unmuted.
+ * The unspecified variant automatically chooses the adequate channel (**).
+ */
+uint8_t audio_headphones_get_mute();
+uint8_t audio_speaker_get_mute();
+uint8_t audio_get_mute();
+
+/* Set the minimum and maximum allowed volume levels for speakers and headphones
+ * respectively. Clamps with hardware limitations. Maximum clamps below the minimum
+ * value, minimum clamps above the maximum. Returns clamped value.
+ */
+float audio_headphones_set_minimum_volume_dB(float vol_dB);
+float audio_headphones_set_maximum_volume_dB(float vol_dB);
+float audio_speaker_set_minimum_volume_dB(float vol_dB);
+float audio_speaker_set_maximum_volume_dB(float vol_dB);
+
+/* Returns the minimum and maximum allowed volume levels for speakers and headphones
+ * respectively. Change with audio_{headphones/speaker}_set_{minimum/maximum}_volume_dB.
+ */
+float audio_headphones_get_minimum_volume_dB();
+float audio_headphones_get_maximum_volume_dB();
+float audio_speaker_get_minimum_volume_dB();
+float audio_speaker_get_maximum_volume_dB();
+
+/* Syntactic sugar for drawing UI: Returns channel volume in a 0..1 range,
+ * scaled into a 0.01..1 range according to the values set with
+ * audio_{headphones_/speaker_/}set_{maximum/minimum}_volume_ and 0 if in a 
+ * fake mute condition.
+ *
+ * The unspecified variant automatically chooses the adequate channel (**).
+ */
+float audio_headphones_get_volume_relative();
+float audio_speaker_get_volume_relative();
+float audio_get_volume_relative();
+
+/* (**) if audio_headphones_are_connected returns 1 the "headphone" variant
+ *      is chosen, else the "speaker" variant is chosen.
+ */
+
+/*
+HEADPHONE PORT POLICY
+
+Under normal circumstances it is an important feature to have a reliable speaker
+mute when plugging in headphones. However, since the headphone port on the badge
+can also be used for badge link, there are legimate cases where it is desirable to
+have the speakers unmuted while a cable is plugged into the jack.
+
+As a person who plugs in the headphones on the tram, doesn't put them on, turns on
+music to check if it's not accidentially playing on speakers and then finally puts
+on headphones (temporarily, of course, intermittent checks if the speakers didn't
+magically turn on are scheduled according to our general anxiety level) we wish to
+make it difficult to accidentially have sound coming from the speakers.
+
+Our proposed logic is as follows (excluding boot conditions):
+
+1) Badge link TX cannot be enabled for any of the headphone jack pins without a
+cable detected in the jack. This is to protect users from plugging in headphones
+while badge link is active and receiving a short but potentially very loud burst
+of digital data before the software can react to the state change.
+
+2) If the software detects that the headphone jack has changed from unplugged to
+plugged it *always* turns off speakers, no exceptions.
+
+3) If a user wishes to TX on headphone badge link, they must confirm a warning that
+having headphones plugged in may potentially cause hearing damage *every time*.
+
+4) If a user wishes to RX or TX on headphone badge link while playing sound on the
+onboard speakers, they must confirm a warning *every time*.
+
+We understand that these means seem extreme, but we find them to be a sensible
+default configuration to make sure people can safely operate the device without
+needing to refer to a manual.
+
+(TX here means any state that is not constantly ~GND with whatever impedance.
+While there are current limiting resistors (value TBD at the time of writing, but
+presumably 100R-470R) in series with the GPIOs, they still can generate quite some
+volume with standard 40Ohm-ish headphones. Ideally the analog switch will never
+switch to the GPIOs without a cable plugged in.)
+*/
diff --git a/components/badge23/include/badge23/lock.h b/components/badge23/include/badge23/lock.h
new file mode 100644
index 0000000000..eb52077c7b
--- /dev/null
+++ b/components/badge23/include/badge23/lock.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#include <freertos/FreeRTOS.h>
+#include <freertos/semphr.h>
+
+SemaphoreHandle_t mutex_i2c;
diff --git a/components/badge23/spio.c b/components/badge23/spio.c
index c93ca9d2eb..e93bc90456 100644
--- a/components/badge23/spio.c
+++ b/components/badge23/spio.c
@@ -3,6 +3,7 @@
 #include "badge23_hwconfig.h"
 #include "stdint.h"
 #include "badge23/spio.h"
+#include "badge23/lock.h"
 
 static int8_t leftbutton = 0;
 static int8_t rightbutton = 0;
@@ -94,7 +95,11 @@ static void _init_buttons(){
 
 void update_button_state(){
     uint8_t port;
+
+    xSemaphoreTake(mutex_i2c, portMAX_DELAY);
     esp_err_t ret = i2c_master_read_from_device(I2C_MASTER_NUM, 0b1101101, &port, sizeof(port), TIMEOUT_MS / portTICK_PERIOD_MS);
+    xSemaphoreGive(mutex_i2c);
+
     uint8_t rr = port & (1ULL << RIGHT_BUTTON_RIGHT);
     uint8_t rm = port & (1ULL << RIGHT_BUTTON_MID);
     uint8_t rl = port & (1ULL << RIGHT_BUTTON_LEFT);
diff --git a/python_payload/demo_menu.py b/python_payload/demo_menu.py
index 25c024a719..b1de86295a 100644
--- a/python_payload/demo_menu.py
+++ b/python_payload/demo_menu.py
@@ -2,6 +2,7 @@ import menu
 import event
 import hardware
 import control
+import audio
 import application
 
 import demo_worms,demo_sparabo,cap_touch_demo, melodic_demo, harmonic_demo
@@ -10,7 +11,7 @@ import menu_settings,menu_tinysynth
 import time
 
 hardware.captouch_autocalib()
-hardware.set_global_volume_dB(0)
+audio.set_volume_dB(0)
 
 
 menu_demo = menu.Menu("demo")
diff --git a/python_payload/demo_sparabo.py b/python_payload/demo_sparabo.py
index 36ead3a4aa..f22a12a6aa 100644
--- a/python_payload/demo_sparabo.py
+++ b/python_payload/demo_sparabo.py
@@ -8,6 +8,7 @@ from synth import tinysynth
 import application
 import ui
 
+import audio
 
 popcorn = [9,7,9,5,0,5,-3,999]
 
@@ -37,7 +38,7 @@ def on_step(data):
 
 class AppSparabo(application.Application):
     def on_init(self):
-        hardware.set_global_volume_dB(0)
+        audio.set_volume_dB(0)
         
         self.synth = tinysynth(440,1)
         self.synth.decay(25)
@@ -55,4 +56,4 @@ class AppSparabo(application.Application):
         self.sequencer.stop()
 
     
-app = AppSparabo("sequencer")
\ No newline at end of file
+app = AppSparabo("sequencer")
diff --git a/python_payload/menu_settings.py b/python_payload/menu_settings.py
index 860c7e1a75..6e4b985724 100644
--- a/python_payload/menu_settings.py
+++ b/python_payload/menu_settings.py
@@ -1,5 +1,6 @@
 import menu
 import event
+import audio
 import control
 import ui
 import hardware
@@ -28,7 +29,7 @@ def set_controls_overlay(value):
 def set_volume(value):
     db = int(value*60-40)
     print("DB",db)
-    hardware.set_global_volume_dB(db)
+    audio.set_volume_dB(db)
     
 
 def get_menu():
@@ -48,4 +49,4 @@ def get_menu():
 
     return m
 
-m = get_menu()
\ No newline at end of file
+m = get_menu()
diff --git a/python_payload/menu_tinysynth.py b/python_payload/menu_tinysynth.py
index 0d012b4442..a77d138896 100644
--- a/python_payload/menu_tinysynth.py
+++ b/python_payload/menu_tinysynth.py
@@ -5,6 +5,7 @@ import event
 import control
 import ui
 import hardware
+import audio
 
 
 ui_input = ui.Icon("")
@@ -20,7 +21,7 @@ def set_play(value):
 def set_volume(value):
     db = int(value*60-40)
     print("DB",db)
-    hardware.set_global_volume_dB(db)
+    audio.set_volume_dB(db)
 
 def set_frequency(value):
     f = 440+value*440
@@ -42,4 +43,4 @@ def get_menu():
 
 synth = tinysynth(440,0)
 
-m = get_menu()
\ No newline at end of file
+m = get_menu()
diff --git a/python_payload/utils.py b/python_payload/utils.py
index 2341cf9a0c..060d754b65 100644
--- a/python_payload/utils.py
+++ b/python_payload/utils.py
@@ -1,5 +1,6 @@
 import time
 from hardware import *
+import audio
 
 RED = 0b1111100000000000
 GREEN = 0b0000011111100000
@@ -24,12 +25,7 @@ def long_bottom_petal_captouch_blocking(num, ms):
     return False
 
 def draw_volume_slider(ctx, volume):
-    length = 96 + ((volume - 20) * 1.6)
-    if length > 96:
-        length = 96
-    if length < 0:
-        length = 0
-    length = int(length)
+    length = int(96*volume)
 
     ctx.rgb(0,0,0)#dummy
     ctx.round_rectangle(-49,41,98,8,3).fill()#dummy idk
diff --git a/usermodule/micropython.cmake b/usermodule/micropython.cmake
index 0be5f99f67..94c79a75f7 100644
--- a/usermodule/micropython.cmake
+++ b/usermodule/micropython.cmake
@@ -6,6 +6,7 @@ add_library(usermod_badge23 INTERFACE)
 
 target_sources(usermod_badge23 INTERFACE
     ${CMAKE_CURRENT_LIST_DIR}/mp_hardware.c
+    ${CMAKE_CURRENT_LIST_DIR}/mp_audio.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_synth.c
     ${CMAKE_CURRENT_LIST_DIR}/mp_kernel.c
 )
diff --git a/usermodule/mp_audio.c b/usermodule/mp_audio.c
new file mode 100644
index 0000000000..b2300b88d5
--- /dev/null
+++ b/usermodule/mp_audio.c
@@ -0,0 +1,234 @@
+// probably doesn't need all of these idk
+#include <stdio.h>
+#include <string.h>
+
+#include "py/runtime.h"
+#include "py/mphal.h"
+#include "mphalport.h"
+#include "modmachine.h"
+#include "extmod/virtpin.h"
+#include "machine_rtc.h"
+#include "py/builtin.h"
+#include "py/runtime.h"
+
+#include "badge23/audio.h"
+#include "badge23_hwconfig.h"
+
+// documentation: these are all super thin wrappers for the c api in components/badge23/include/badge23/audio.h
+
+STATIC mp_obj_t mp_headset_is_connected() {
+    return mp_obj_new_int(audio_headset_is_connected());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_headset_is_connected_obj, mp_headset_is_connected);
+
+STATIC mp_obj_t mp_headphones_are_connected() {
+    return mp_obj_new_int(audio_headphones_are_connected());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_headphones_are_connected_obj, mp_headphones_are_connected);
+
+STATIC mp_obj_t mp_headphones_detection_override(mp_obj_t enable) {
+    audio_headphones_detection_override(mp_obj_get_int(enable));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_headphones_detection_override_obj, mp_headphones_detection_override);
+
+
+
+STATIC mp_obj_t mp_headphones_set_volume_dB(mp_obj_t vol_dB) {
+    return mp_obj_new_float(audio_headphones_set_volume_dB(mp_obj_get_float(vol_dB)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_headphones_set_volume_dB_obj, mp_headphones_set_volume_dB);
+
+STATIC mp_obj_t mp_speaker_set_volume_dB(mp_obj_t vol_dB) {
+    return mp_obj_new_float(audio_speaker_set_volume_dB(mp_obj_get_float(vol_dB)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_speaker_set_volume_dB_obj, mp_speaker_set_volume_dB);
+
+STATIC mp_obj_t mp_set_volume_dB(mp_obj_t vol_dB) {
+    return mp_obj_new_float(audio_set_volume_dB(mp_obj_get_float(vol_dB)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_set_volume_dB_obj, mp_set_volume_dB);
+
+
+
+STATIC mp_obj_t mp_headphones_adjust_volume_dB(mp_obj_t vol_dB) {
+    return mp_obj_new_float(audio_headphones_adjust_volume_dB(mp_obj_get_float(vol_dB)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_headphones_adjust_volume_dB_obj, mp_headphones_adjust_volume_dB);
+
+STATIC mp_obj_t mp_speaker_adjust_volume_dB(mp_obj_t vol_dB) {
+    return mp_obj_new_float(audio_speaker_adjust_volume_dB(mp_obj_get_float(vol_dB)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_speaker_adjust_volume_dB_obj, mp_speaker_adjust_volume_dB);
+
+STATIC mp_obj_t mp_adjust_volume_dB(mp_obj_t vol_dB) {
+    return mp_obj_new_float(audio_adjust_volume_dB(mp_obj_get_float(vol_dB)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_adjust_volume_dB_obj, mp_adjust_volume_dB);
+
+
+
+STATIC mp_obj_t mp_headphones_get_volume_dB() {
+    return mp_obj_new_float(audio_headphones_get_volume_dB());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_headphones_get_volume_dB_obj, mp_headphones_get_volume_dB);
+
+STATIC mp_obj_t mp_speaker_get_volume_dB() {
+    return mp_obj_new_float(audio_speaker_get_volume_dB());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_speaker_get_volume_dB_obj, mp_speaker_get_volume_dB);
+
+STATIC mp_obj_t mp_get_volume_dB() {
+    return mp_obj_new_float(audio_get_volume_dB());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_volume_dB_obj, mp_get_volume_dB);
+
+
+
+STATIC mp_obj_t mp_headphones_get_mute() {
+    return mp_obj_new_int(audio_headphones_get_mute());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_headphones_get_mute_obj, mp_headphones_get_mute);
+
+STATIC mp_obj_t mp_speaker_get_mute() {
+    return mp_obj_new_int(audio_speaker_get_mute());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_speaker_get_mute_obj, mp_speaker_get_mute);
+
+STATIC mp_obj_t mp_get_mute() {
+    return mp_obj_new_int(audio_get_mute());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_mute_obj, mp_get_mute);
+
+
+
+STATIC mp_obj_t mp_headphones_set_mute(mp_obj_t mute) {
+    audio_headphones_set_mute(mp_obj_get_int(mute));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_headphones_set_mute_obj, mp_headphones_set_mute);
+
+STATIC mp_obj_t mp_speaker_set_mute(mp_obj_t mute) {
+    audio_speaker_set_mute(mp_obj_get_int(mute));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_speaker_set_mute_obj, mp_speaker_set_mute);
+
+STATIC mp_obj_t mp_set_mute(mp_obj_t mute) {
+    audio_set_mute(mp_obj_get_int(mute));
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_set_mute_obj, mp_set_mute);
+
+
+
+STATIC mp_obj_t mp_headphones_set_minimum_volume_dB(mp_obj_t vol_dB) {
+    return mp_obj_new_float(audio_headphones_set_minimum_volume_dB(mp_obj_get_float(vol_dB)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_headphones_set_minimum_volume_dB_obj, mp_headphones_set_minimum_volume_dB);
+
+STATIC mp_obj_t mp_speaker_set_minimum_volume_dB(mp_obj_t vol_dB) {
+    return mp_obj_new_float(audio_speaker_set_minimum_volume_dB(mp_obj_get_float(vol_dB)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_speaker_set_minimum_volume_dB_obj, mp_speaker_set_minimum_volume_dB);
+
+STATIC mp_obj_t mp_headphones_set_maximum_volume_dB(mp_obj_t vol_dB) {
+    return mp_obj_new_float(audio_headphones_set_maximum_volume_dB(mp_obj_get_float(vol_dB)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_headphones_set_maximum_volume_dB_obj, mp_headphones_set_maximum_volume_dB);
+
+STATIC mp_obj_t mp_speaker_set_maximum_volume_dB(mp_obj_t vol_dB) {
+    return mp_obj_new_float(audio_speaker_set_maximum_volume_dB(mp_obj_get_float(vol_dB)));
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mp_speaker_set_maximum_volume_dB_obj, mp_speaker_set_maximum_volume_dB);
+
+
+
+STATIC mp_obj_t mp_headphones_get_minimum_volume_dB() {
+    return mp_obj_new_float(audio_headphones_get_minimum_volume_dB());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_headphones_get_minimum_volume_dB_obj, mp_headphones_get_minimum_volume_dB);
+
+STATIC mp_obj_t mp_speaker_get_minimum_volume_dB() {
+    return mp_obj_new_float(audio_speaker_get_minimum_volume_dB());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_speaker_get_minimum_volume_dB_obj, mp_speaker_get_minimum_volume_dB);
+
+STATIC mp_obj_t mp_headphones_get_maximum_volume_dB() {
+    return mp_obj_new_float(audio_headphones_get_maximum_volume_dB());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_headphones_get_maximum_volume_dB_obj, mp_headphones_get_maximum_volume_dB);
+
+STATIC mp_obj_t mp_speaker_get_maximum_volume_dB() {
+    return mp_obj_new_float(audio_speaker_get_maximum_volume_dB());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_speaker_get_maximum_volume_dB_obj, mp_speaker_get_maximum_volume_dB);
+
+
+
+STATIC mp_obj_t mp_headphones_get_volume_relative() {
+    return mp_obj_new_float(audio_headphones_get_volume_relative());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_headphones_get_volume_relative_obj, mp_headphones_get_volume_relative);
+
+STATIC mp_obj_t mp_speaker_get_volume_relative() {
+    return mp_obj_new_float(audio_speaker_get_volume_relative());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_speaker_get_volume_relative_obj, mp_speaker_get_volume_relative);
+
+STATIC mp_obj_t mp_get_volume_relative() {
+    return mp_obj_new_float(audio_get_volume_relative());
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_get_volume_relative_obj, mp_get_volume_relative);
+
+
+STATIC const mp_rom_map_elem_t mp_module_audio_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audio) },
+    { MP_ROM_QSTR(MP_QSTR_headset_is_connected), MP_ROM_PTR(&mp_headset_is_connected_obj) },
+    { MP_ROM_QSTR(MP_QSTR_headphones_are_connected), MP_ROM_PTR(&mp_headphones_are_connected_obj) },
+    { MP_ROM_QSTR(MP_QSTR_headphones_detection_override), MP_ROM_PTR(&mp_headphones_detection_override_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_headphones_set_volume_dB), MP_ROM_PTR(&mp_headphones_set_volume_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_speaker_set_volume_dB), MP_ROM_PTR(&mp_speaker_set_volume_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_set_volume_dB), MP_ROM_PTR(&mp_set_volume_dB_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_headphones_adjust_volume_dB), MP_ROM_PTR(&mp_headphones_adjust_volume_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_speaker_adjust_volume_dB), MP_ROM_PTR(&mp_speaker_adjust_volume_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_adjust_volume_dB), MP_ROM_PTR(&mp_adjust_volume_dB_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_headphones_get_volume_dB), MP_ROM_PTR(&mp_headphones_get_volume_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_speaker_get_volume_dB), MP_ROM_PTR(&mp_speaker_get_volume_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_get_volume_dB), MP_ROM_PTR(&mp_get_volume_dB_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_headphones_get_mute), MP_ROM_PTR(&mp_headphones_get_mute_obj) },
+    { MP_ROM_QSTR(MP_QSTR_speaker_get_mute), MP_ROM_PTR(&mp_speaker_get_mute_obj) },
+    { MP_ROM_QSTR(MP_QSTR_get_mute), MP_ROM_PTR(&mp_get_mute_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_headphones_set_mute), MP_ROM_PTR(&mp_headphones_set_mute_obj) },
+    { MP_ROM_QSTR(MP_QSTR_speaker_set_mute), MP_ROM_PTR(&mp_speaker_set_mute_obj) },
+    { MP_ROM_QSTR(MP_QSTR_set_mute), MP_ROM_PTR(&mp_set_mute_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_headphones_set_minimum_volume_dB), MP_ROM_PTR(&mp_headphones_set_minimum_volume_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_speaker_set_minimum_volume_dB), MP_ROM_PTR(&mp_speaker_set_minimum_volume_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_headphones_set_maximum_volume_dB), MP_ROM_PTR(&mp_headphones_set_maximum_volume_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_speaker_set_maximum_volume_dB), MP_ROM_PTR(&mp_speaker_set_maximum_volume_dB_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_headphones_get_minimum_volume_dB), MP_ROM_PTR(&mp_headphones_get_minimum_volume_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_speaker_get_minimum_volume_dB), MP_ROM_PTR(&mp_speaker_get_minimum_volume_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_headphones_get_maximum_volume_dB), MP_ROM_PTR(&mp_headphones_get_maximum_volume_dB_obj) },
+    { MP_ROM_QSTR(MP_QSTR_speaker_get_maximum_volume_dB), MP_ROM_PTR(&mp_speaker_get_maximum_volume_dB_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_headphones_get_volume_relative), MP_ROM_PTR(&mp_headphones_get_volume_relative_obj) },
+    { MP_ROM_QSTR(MP_QSTR_speaker_get_volume_relative), MP_ROM_PTR(&mp_speaker_get_volume_relative_obj) },
+    { MP_ROM_QSTR(MP_QSTR_get_volume_relative), MP_ROM_PTR(&mp_get_volume_relative_obj) },
+};
+
+STATIC MP_DEFINE_CONST_DICT(mp_module_audio_globals, mp_module_audio_globals_table);
+
+const mp_obj_module_t mp_module_audio = {
+    .base = { &mp_type_module },
+    .globals = (mp_obj_dict_t *)&mp_module_audio_globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_audio, mp_module_audio);
+
diff --git a/usermodule/mp_hardware.c b/usermodule/mp_hardware.c
index f0fc7fe79b..a692e884ec 100644
--- a/usermodule/mp_hardware.c
+++ b/usermodule/mp_hardware.c
@@ -136,10 +136,9 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_0(mp_right_button_get_obj, mp_right_button_get);
 
 
 STATIC mp_obj_t mp_set_global_volume_dB(size_t n_args, const mp_obj_t *args) {
-    mp_float_t x = mp_obj_get_float(args[0]);
-    int8_t d = x;
-    set_global_vol_dB(d);
-    mp_float_t l = x;
+    //TODO: DEPRECATE
+    mp_float_t d = mp_obj_get_float(args[0]);
+    audio_set_volume_dB(d);
     return mp_const_none;
 }
 STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_set_global_volume_dB_obj, 1, 2, mp_set_global_volume_dB);
-- 
GitLab