diff --git a/common/wireplumber.c b/common/wireplumber.c new file mode 100644 index 000000000..41fae769e --- /dev/null +++ b/common/wireplumber.c @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include +#include "json/json.h" +#include "wireplumber.h" + +#include + +#include "common.h" +#include "log.h" + +static int parse_json_to_sinks(const char *json_output, Sink **sinks, int *count); + +int get_sinks(Sink **sinks, int *count) { + printf("Executing pw-dump command to retrieve sinks...\n"); + + FILE *fp = popen("pw-dump", "r"); + if (!fp) { + LOG_ERROR(mux_module, "Failed to run pw-dump: %s", strerror(errno)); + return -1; + } + + char *json_output = NULL; + size_t json_size = 0; + char buffer[512]; + + while (fgets(buffer, sizeof(buffer), fp)) { + size_t line_length = strlen(buffer); + char *new_output = realloc(json_output, json_size + line_length + 1); + if (!new_output) { + LOG_ERROR(mux_module, "Failed to allocate memory for JSON buffer: %s", strerror(errno)); + free(json_output); + pclose(fp); + return -1; + } + json_output = new_output; + memcpy(json_output + json_size, buffer, line_length); + json_size += line_length; + json_output[json_size] = '\0'; + } + pclose(fp); + + if (!json_output) { + fprintf(stderr, "Failed to retrieve JSON output from pw-dump\n"); + return -1; + } + + int result = parse_json_to_sinks(json_output, sinks, count); + free(json_output); + + return result; +} + +static int parse_json_to_sinks(const char *json_output, Sink **sinks, int *count) { + struct json root = json_parse(json_output); + if (!json_exists(root) || json_type(root) != JSON_ARRAY) { + fprintf(stderr, "Failed to parse JSON or JSON is not an array\n"); + return -1; + } + + *sinks = NULL; + *count = 0; + + size_t total_items = json_array_count(root); + for (size_t i = 0; i < total_items; i++) { + struct json item = json_array_get(root, i); + if (json_type(item) != JSON_OBJECT) { + continue; + } + + struct json info = json_object_get(item, "info"); + if (!json_exists(info) || json_type(info) != JSON_OBJECT) { + continue; + } + + struct json props = json_object_get(info, "props"); + if (!json_exists(props) || json_type(props) != JSON_OBJECT) { + continue; + } + + struct json media_class = json_object_get(props, "media.class"); + if (!json_exists(media_class) || json_type(media_class) != JSON_STRING || + json_string_compare(media_class, "Audio/Sink") != 0) { + continue; + } + + struct json description = json_object_get(props, "node.description"); + struct json nick = json_object_get(props, "node.nick"); + struct json name = json_object_get(props, "node.name"); + struct json object_id = json_object_get(item, "id"); + + if (!json_exists(description) || !json_exists(object_id) || json_type(object_id) != JSON_NUMBER) { + continue; + } + + Sink sink = {0}; + sink.id = json_int(object_id); + json_string_copy(description, sink.description, sizeof(sink.description)); + if (json_exists(nick)) { + json_string_copy(nick, sink.nick, sizeof(sink.nick)); + } else { + strncpy(sink.nick, "Unknown", sizeof(sink.nick)); + } + if (json_exists(name)) { + json_string_copy(name, sink.name, sizeof(sink.name)); + } else { + strncpy(sink.name, "Unknown", sizeof(sink.name)); + } + + // Allocate memory for the new sink + Sink *new_sinks = realloc(*sinks, (*count + 1) * sizeof(Sink)); + if (!new_sinks) { + LOG_ERROR(mux_module, "Failed to allocate memory for sinks array: %s", strerror(errno)); + free(*sinks); + return -1; + } + + *sinks = new_sinks; + (*sinks)[*count] = sink; + (*count)++; + } + + return 0; +} + +int get_default_sink_id(int *sink_id) { + const char* command = "wpctl status | grep -A 5 Sinks | grep '\\*' | sed 's/ \\|.*\\* //' | cut -f1 -d'.'"; + FILE *fp = popen(command, "r"); + if (fp == NULL) { + LOG_ERROR(mux_module, "Failed to run command: %s", strerror(errno)); + return -1; + } + + char buffer[128]; + if (fgets(buffer, sizeof(buffer), fp) != NULL) { + buffer[strcspn(buffer, "\n")] = '\0'; + *sink_id = atoi(buffer); + } else { + LOG_ERROR(mux_module, "Failed to read sink ID: %s", strerror(errno)); + pclose(fp); + return -1; + } + + pclose(fp); + return 0; +} + +int set_default_sink(int sink_id) { + char command[256]; + snprintf(command, sizeof(command), "wpctl set-default %d", sink_id); + LOG_INFO(mux_module, "Executing command: %s", command); + int result = system(command); + if (result != 0) { + LOG_ERROR(mux_module, "Failed to execute command: %s", strerror(errno)); + } + LOG_INFO(mux_module, "Command result: %d", result); + return result; +} \ No newline at end of file diff --git a/common/wireplumber.h b/common/wireplumber.h new file mode 100644 index 000000000..f9a17010b --- /dev/null +++ b/common/wireplumber.h @@ -0,0 +1,13 @@ +#pragma once + +typedef struct +{ + int id; + char description[256]; + char nick[256]; + char name[256]; +} Sink; + +int get_sinks(Sink** sinks, int* count); +int get_default_sink_id(int *sink_id); +int set_default_sink(int sink_id); diff --git a/module/muxtweakgen.c b/module/muxtweakgen.c index ab31208fe..a76127542 100755 --- a/module/muxtweakgen.c +++ b/module/muxtweakgen.c @@ -8,6 +8,8 @@ #include #include #include +#include + #include "../common/common.h" #include "../common/options.h" #include "../common/theme.h" @@ -17,6 +19,7 @@ #include "../common/kiosk.h" #include "../common/input.h" #include "../common/input/list_nav.h" +#include "../common/wireplumber.h" char *mux_module; static int js_fd; @@ -45,14 +48,14 @@ lv_obj_t *kiosk_image = NULL; int progress_onscreen = -1; -int hidden_original, bgm_original, sound_original, startup_original, colour_original, brightness_original; +int hidden_original, bgm_original, sound_original, startup_original, colour_original, brightness_original, audio_sink_original; lv_group_t *ui_group; lv_group_t *ui_group_value; lv_group_t *ui_group_glyph; lv_group_t *ui_group_panel; -#define UI_COUNT 10 +#define UI_COUNT 11 lv_obj_t *ui_objects[UI_COUNT]; lv_obj_t *ui_mux_panels[5]; @@ -68,6 +71,7 @@ void show_help(lv_obj_t *element_focused) { "at the start of a file or folder name to hide it")}, {ui_lblBGM, TS("Toggle the background music of the frontend - This will stop if content is launched")}, {ui_lblSound, TS("Toggle the navigation sound of the frontend if the current theme supports it")}, + {ui_lblAudioSink, TS("Change the audio output sink")}, {ui_lblStartup, TS("Change where the device will start up into")}, {ui_lblColour, TS("Change the colour temperature of the display if the device supports it")}, {ui_lblBrightness, TS("Change the brightness of the device to a specific level")}, @@ -93,6 +97,69 @@ void show_help(lv_obj_t *element_focused) { TS(lv_label_get_text(element_focused)), message); } +void init_audio_sink_dropdown() { + LOG_DEBUG(mux_module, "Initializing audio sink dropdown..."); + + Sink *sinks = NULL; + int count = 0; + + if (get_sinks(&sinks, &count) != 0) { + LOG_ERROR(mux_module, "Failed to retrieve audio sinks"); + return; + } + + lv_dropdown_clear_options(ui_droAudioSink); + + for (int i = 0; i < count; i++) { + char dropdown_option[128]; + snprintf(dropdown_option, sizeof(dropdown_option), "%d: %s", sinks[i].id, sinks[i].description); + LOG_DEBUG(mux_module, "Adding sink to dropdown: %s", dropdown_option); + lv_dropdown_add_option(ui_droAudioSink, dropdown_option, LV_DROPDOWN_POS_LAST); + } + + int default_sink_id = -1; + if (get_default_sink_id(&default_sink_id) == 0) { + LOG_DEBUG(mux_module, "Default sink ID: %d", default_sink_id); + for (int i = 0; i < count; i++) { + if (sinks[i].id == default_sink_id) { + audio_sink_original = i; + LOG_DEBUG(mux_module, "Setting default sink to index: %d", i); + lv_dropdown_set_selected(ui_droAudioSink, i); + break; + } + } + } else { + LOG_ERROR(mux_module, "Failed to retrieve default sink ID."); + } + + free(sinks); +} + +static void update_default_sink(int selected_index) { + Sink *sinks = NULL; + int count = 0; + + if (get_sinks(&sinks, &count) != 0) { + LOG_ERROR(mux_module, "Failed to retrieve sinks."); + return; + } + + if (selected_index >= 0 && selected_index < count) { + int sink_id = sinks[selected_index].id; + LOG_DEBUG(mux_module, "Setting default sink ID: %d", sink_id); + + if (set_default_sink(sink_id) == 0) { + LOG_DEBUG(mux_module, "Successfully set default sink."); + } else { + LOG_ERROR(mux_module, "Failed to set default sink."); + } + } else { + LOG_ERROR(mux_module, "Invalid selected audio sink index: %d", selected_index); + } + + free(sinks); +} + static void dropdown_event_handler(lv_event_t *e) { lv_event_code_t code = lv_event_get_code(e); lv_obj_t * obj = lv_event_get_target(e); @@ -108,6 +175,7 @@ void elements_events_init() { ui_droHidden, ui_droBGM, ui_droSound, + ui_droAudioSink, ui_droStartup, ui_droColour, ui_droBrightness @@ -125,6 +193,7 @@ void init_dropdown_settings() { startup_original = lv_dropdown_get_selected(ui_droStartup); colour_original = lv_dropdown_get_selected(ui_droColour); brightness_original = lv_dropdown_get_selected(ui_droBrightness); + init_audio_sink_dropdown(); } void restore_tweak_options() { @@ -199,6 +268,12 @@ void save_tweak_options() { write_text_to_file("/run/muos/global/settings/general/sound", "w", INT, idx_sound); } + int idx_audio_sink = lv_dropdown_get_selected(ui_droAudioSink); + if (idx_audio_sink != audio_sink_original) { + is_modified++; + update_default_sink(idx_audio_sink); + } + if (lv_dropdown_get_selected(ui_droStartup) != startup_original) { is_modified++; write_text_to_file("/run/muos/global/settings/general/startup", "w", CHAR, idx_startup); @@ -226,6 +301,7 @@ void init_navigation_groups() { ui_pnlHidden, ui_pnlBGM, ui_pnlSound, + ui_pnlAudioSink, ui_pnlStartup, ui_pnlColour, ui_pnlBrightness, @@ -238,18 +314,20 @@ void init_navigation_groups() { ui_objects[0] = ui_lblHidden; ui_objects[1] = ui_lblBGM; ui_objects[2] = ui_lblSound; - ui_objects[3] = ui_lblStartup; - ui_objects[4] = ui_lblColour; - ui_objects[5] = ui_lblBrightness; - ui_objects[6] = ui_lblHDMI; - ui_objects[7] = ui_lblPower; - ui_objects[8] = ui_lblInterface; - ui_objects[9] = ui_lblAdvanced; + ui_objects[3] = ui_lblAudioSink; + ui_objects[4] = ui_lblStartup; + ui_objects[5] = ui_lblColour; + ui_objects[6] = ui_lblBrightness; + ui_objects[7] = ui_lblHDMI; + ui_objects[8] = ui_lblPower; + ui_objects[9] = ui_lblInterface; + ui_objects[10] = ui_lblAdvanced; lv_obj_t *ui_objects_value[] = { ui_droHidden, ui_droBGM, ui_droSound, + ui_droAudioSink, ui_droStartup, ui_droColour, ui_droBrightness, @@ -263,6 +341,7 @@ void init_navigation_groups() { ui_icoHidden, ui_icoBGM, ui_icoSound, + ui_icoAudioSink, ui_icoStartup, ui_icoColour, ui_icoBrightness, @@ -275,6 +354,7 @@ void init_navigation_groups() { apply_theme_list_panel(&theme, &device, ui_pnlHidden); apply_theme_list_panel(&theme, &device, ui_pnlBGM); apply_theme_list_panel(&theme, &device, ui_pnlSound); + apply_theme_list_panel(&theme, &device, ui_pnlAudioSink); apply_theme_list_panel(&theme, &device, ui_pnlStartup); apply_theme_list_panel(&theme, &device, ui_pnlColour); apply_theme_list_panel(&theme, &device, ui_pnlBrightness); @@ -286,6 +366,7 @@ void init_navigation_groups() { apply_theme_list_item(&theme, ui_lblHidden, TS("Show Hidden Content"), false, true); apply_theme_list_item(&theme, ui_lblBGM, TS("Background Music"), false, true); apply_theme_list_item(&theme, ui_lblSound, TS("Navigation Sound"), false, true); + apply_theme_list_item(&theme, ui_lblAudioSink, TS("Audio Sink"), false, true); apply_theme_list_item(&theme, ui_lblStartup, TS("Device Startup"), false, true); apply_theme_list_item(&theme, ui_lblColour, TS("Colour Temperature"), false, true); apply_theme_list_item(&theme, ui_lblBrightness, TS("Brightness"), false, true); @@ -297,6 +378,7 @@ void init_navigation_groups() { apply_theme_list_glyph(&theme, ui_icoHidden, mux_module, "hidden"); apply_theme_list_glyph(&theme, ui_icoBGM, mux_module, "bgm"); apply_theme_list_glyph(&theme, ui_icoSound, mux_module, "sound"); + apply_theme_list_glyph(&theme, ui_icoAudioSink, mux_module, "sound"); apply_theme_list_glyph(&theme, ui_icoStartup, mux_module, "startup"); apply_theme_list_glyph(&theme, ui_icoColour, mux_module, "colour"); apply_theme_list_glyph(&theme, ui_icoBrightness, mux_module, "brightness"); @@ -308,6 +390,7 @@ void init_navigation_groups() { apply_theme_list_drop_down(&theme, ui_droHidden, NULL); apply_theme_list_drop_down(&theme, ui_droBGM, NULL); apply_theme_list_drop_down(&theme, ui_droSound, NULL); + apply_theme_list_drop_down(&theme, ui_droAudioSink, NULL); apply_theme_list_drop_down(&theme, ui_droStartup, NULL); apply_theme_list_drop_down(&theme, ui_droColour, NULL); @@ -500,6 +583,7 @@ void init_elements() { lv_obj_set_user_data(ui_lblHidden, "hidden"); lv_obj_set_user_data(ui_lblBGM, "bgm"); lv_obj_set_user_data(ui_lblSound, "sound"); + lv_obj_set_user_data(ui_lblAudioSink, "audiosink"); lv_obj_set_user_data(ui_lblStartup, "startup"); lv_obj_set_user_data(ui_lblColour, "colour"); lv_obj_set_user_data(ui_lblBrightness, "brightness"); diff --git a/module/ui/ui_muxtweakgen.c b/module/ui/ui_muxtweakgen.c index 7c60e4218..75bbe7e67 100755 --- a/module/ui/ui_muxtweakgen.c +++ b/module/ui/ui_muxtweakgen.c @@ -3,6 +3,7 @@ lv_obj_t *ui_pnlHidden; lv_obj_t *ui_pnlBGM; lv_obj_t *ui_pnlSound; +lv_obj_t *ui_pnlAudioSink; lv_obj_t *ui_pnlStartup; lv_obj_t *ui_pnlColour; lv_obj_t *ui_pnlBrightness; @@ -14,6 +15,7 @@ lv_obj_t *ui_pnlAdvanced; lv_obj_t *ui_lblHidden; lv_obj_t *ui_lblBGM; lv_obj_t *ui_lblSound; +lv_obj_t *ui_lblAudioSink; lv_obj_t *ui_lblStartup; lv_obj_t *ui_lblColour; lv_obj_t *ui_lblBrightness; @@ -25,6 +27,7 @@ lv_obj_t *ui_lblAdvanced; lv_obj_t *ui_icoHidden; lv_obj_t *ui_icoBGM; lv_obj_t *ui_icoSound; +lv_obj_t *ui_icoAudioSink; lv_obj_t *ui_icoStartup; lv_obj_t *ui_icoColour; lv_obj_t *ui_icoBrightness; @@ -36,6 +39,7 @@ lv_obj_t *ui_icoAdvanced; lv_obj_t *ui_droHidden; lv_obj_t *ui_droBGM; lv_obj_t *ui_droSound; +lv_obj_t *ui_droAudioSink; lv_obj_t *ui_droStartup; lv_obj_t *ui_droColour; lv_obj_t *ui_droBrightness; @@ -48,6 +52,7 @@ void ui_init(lv_obj_t *ui_pnlContent) { ui_pnlHidden = lv_obj_create(ui_pnlContent); ui_pnlBGM = lv_obj_create(ui_pnlContent); ui_pnlSound = lv_obj_create(ui_pnlContent); + ui_pnlAudioSink = lv_obj_create(ui_pnlContent); ui_pnlStartup = lv_obj_create(ui_pnlContent); ui_pnlColour = lv_obj_create(ui_pnlContent); ui_pnlBrightness = lv_obj_create(ui_pnlContent); @@ -59,6 +64,7 @@ void ui_init(lv_obj_t *ui_pnlContent) { ui_lblHidden = lv_label_create(ui_pnlHidden); ui_lblBGM = lv_label_create(ui_pnlBGM); ui_lblSound = lv_label_create(ui_pnlSound); + ui_lblAudioSink = lv_label_create(ui_pnlAudioSink); ui_lblStartup = lv_label_create(ui_pnlStartup); ui_lblColour = lv_label_create(ui_pnlColour); ui_lblBrightness = lv_label_create(ui_pnlBrightness); @@ -70,6 +76,7 @@ void ui_init(lv_obj_t *ui_pnlContent) { ui_icoHidden = lv_img_create(ui_pnlHidden); ui_icoBGM = lv_img_create(ui_pnlBGM); ui_icoSound = lv_img_create(ui_pnlSound); + ui_icoAudioSink = lv_img_create(ui_pnlAudioSink); ui_icoStartup = lv_img_create(ui_pnlStartup); ui_icoColour = lv_img_create(ui_pnlColour); ui_icoBrightness = lv_img_create(ui_pnlBrightness); @@ -81,6 +88,7 @@ void ui_init(lv_obj_t *ui_pnlContent) { ui_droHidden = lv_dropdown_create(ui_pnlHidden); ui_droBGM = lv_dropdown_create(ui_pnlBGM); ui_droSound = lv_dropdown_create(ui_pnlSound); + ui_droAudioSink = lv_dropdown_create(ui_pnlAudioSink); ui_droStartup = lv_dropdown_create(ui_pnlStartup); ui_droColour = lv_dropdown_create(ui_pnlColour); ui_droBrightness = lv_dropdown_create(ui_pnlBrightness); diff --git a/module/ui/ui_muxtweakgen.h b/module/ui/ui_muxtweakgen.h index 90650f646..4ef291d8a 100755 --- a/module/ui/ui_muxtweakgen.h +++ b/module/ui/ui_muxtweakgen.h @@ -7,6 +7,7 @@ void ui_init(lv_obj_t *ui_pnlContent); extern lv_obj_t *ui_pnlHidden; extern lv_obj_t *ui_pnlBGM; extern lv_obj_t *ui_pnlSound; +extern lv_obj_t *ui_pnlAudioSink; extern lv_obj_t *ui_pnlStartup; extern lv_obj_t *ui_pnlColour; extern lv_obj_t *ui_pnlBrightness; @@ -18,6 +19,7 @@ extern lv_obj_t *ui_pnlAdvanced; extern lv_obj_t *ui_lblHidden; extern lv_obj_t *ui_lblBGM; extern lv_obj_t *ui_lblSound; +extern lv_obj_t *ui_lblAudioSink; extern lv_obj_t *ui_lblStartup; extern lv_obj_t *ui_lblColour; extern lv_obj_t *ui_lblBrightness; @@ -29,6 +31,7 @@ extern lv_obj_t *ui_lblAdvanced; extern lv_obj_t *ui_icoHidden; extern lv_obj_t *ui_icoBGM; extern lv_obj_t *ui_icoSound; +extern lv_obj_t *ui_icoAudioSink; extern lv_obj_t *ui_icoStartup; extern lv_obj_t *ui_icoColour; extern lv_obj_t *ui_icoBrightness; @@ -40,6 +43,7 @@ extern lv_obj_t *ui_icoAdvanced; extern lv_obj_t *ui_droHidden; extern lv_obj_t *ui_droBGM; extern lv_obj_t *ui_droSound; +extern lv_obj_t *ui_droAudioSink; extern lv_obj_t *ui_droStartup; extern lv_obj_t *ui_droColour; extern lv_obj_t *ui_droBrightness;