/* * studio.c -- Studio Audio Class 2.0 Streaming Control Console * * SPDX-License-Identifier: GPL-2.0-or-later * * UAC2 Mixer Console with 4-channel per-channel volume bars. * Integrates 24-bit/192kHz asynchronous audio path visualization * with live application mixer targeting using native USB Consumer * Control pages via /dev/hidg1. Four audio channels (GAME, DISCORD, * MIC, MUSIC) with per-channel volume bars and hardware mute toggle. * * Manifest ref: Asset VII -- Studio Audio Class 2.0 Streaming Console */ #include #include #include #include #include #include #include #include "h2_ui.h" #include "log_manager.h" #define HID_DEV "/dev/hidg1" #define NUM_CHANNELS 4 typedef enum { MIX_GAME, MIX_DISCORD, MIX_MIC, MIX_MUSIC } AudioChannel; static const char *channel_names[NUM_CHANNELS] = { "GAME AUDIO", "DISCORD CHAT", "MICROPHONE", "STREAM MUSIC" }; static const char *channel_symbols[NUM_CHANNELS] = { LV_SYMBOL_VIDEO, LV_SYMBOL_CHAT, LV_SYMBOL_MICROPHONE, LV_SYMBOL_MUSIC }; static AudioChannel active_channel = MIX_GAME; static int channel_volumes[NUM_CHANNELS] = {80, 70, 90, 50}; static int is_muted = 0; /* UI objects */ static lv_obj_t *channel_labels[NUM_CHANNELS] = {NULL}; static lv_obj_t *bar_bg[NUM_CHANNELS] = {NULL}; static lv_obj_t *bar_fill[NUM_CHANNELS] = {NULL}; static lv_obj_t *vol_labels[NUM_CHANNELS] = {NULL}; static lv_obj_t *active_indicator = NULL; static lv_obj_t *mute_label = NULL; static lv_obj_t *dac_label = NULL; static void send_media_command(int hid_fd, uint8_t usage_code) { uint8_t report[2] = {0}; report[0] = usage_code; write(hid_fd, report, 2); /* Release */ report[0] = 0x00; write(hid_fd, report, 2); } static void render_mixer(void) { for (int i = 0; i < NUM_CHANNELS; i++) { /* Update volume bar width: 140px max at 100% */ int fill_w = (channel_volumes[i] * 140) / 100; lv_obj_set_width(bar_fill[i], fill_w > 0 ? fill_w : 1); /* Color: green normal, red if muted and MIC */ if (is_muted && i == MIX_MIC) { lv_obj_set_style_bg_color(bar_fill[i], lv_color_make(255, 0, 0), LV_PART_MAIN); } else { lv_obj_set_style_bg_color(bar_fill[i], lv_color_make(0, 224, 0), LV_PART_MAIN); } /* Volume percentage text */ char vol_str[8]; snprintf(vol_str, sizeof(vol_str), "%d%%", channel_volumes[i]); lv_label_set_text(vol_labels[i], vol_str); /* Highlight active channel */ if (i == (int)active_channel) { lv_obj_set_style_text_color(channel_labels[i], lv_color_make(0, 255, 255), LV_PART_MAIN); lv_obj_set_style_text_font(channel_labels[i], &lv_font_montserrat_12, LV_PART_MAIN); lv_obj_set_style_border_color(bar_bg[i], lv_color_make(0, 255, 255), LV_PART_MAIN); } else { lv_obj_set_style_text_color(channel_labels[i], COLOR_MUTED, LV_PART_MAIN); lv_obj_set_style_text_font(channel_labels[i], &lv_font_montserrat_10, LV_PART_MAIN); lv_obj_set_style_border_color(bar_bg[i], lv_color_make(50, 55, 70), LV_PART_MAIN); } } /* Mute warning */ if (is_muted) { lv_label_set_text(mute_label, "!! MIC CHANNEL MUTED VIA PANIC BUTTON !!"); lv_obj_set_style_text_color(mute_label, lv_color_make(255, 0, 0), LV_PART_MAIN); } else { lv_label_set_text(mute_label, ""); } } int main(void) { init_h2_graphics_runtime("STUDIO PRO CONSOLE"); LOG_INF("studio module started (UAC2 mixer console)"); lv_obj_t *scr = lv_scr_act(); /* Header */ lv_obj_t *header = lv_obj_create(scr); lv_obj_set_size(header, 320, 34); lv_obj_align(header, LV_ALIGN_TOP_MID, 0, 0); lv_obj_set_style_bg_color(header, lv_color_make(49, 134, 49), LV_PART_MAIN); lv_obj_set_style_border_width(header, 0, LV_PART_MAIN); lv_obj_t *title = lv_label_create(header); lv_label_set_text(title, "STUDIO PRO CONSOLE & DAC"); lv_obj_set_style_text_color(title, lv_color_make(255, 255, 255), LV_PART_MAIN); lv_obj_set_style_text_font(title, &lv_font_montserrat_14, LV_PART_MAIN); lv_obj_align(title, LV_ALIGN_CENTER, 0, 0); /* DAC status line */ dac_label = lv_label_create(scr); lv_label_set_text(dac_label, "DAC LINK: ONLINE | PCM 24-Bit | 192 kHz"); lv_obj_align(dac_label, LV_ALIGN_TOP_MID, 0, 38); lv_obj_set_style_text_color(dac_label, COLOR_ACCENT, LV_PART_MAIN); lv_obj_set_style_text_font(dac_label, &lv_font_montserrat_10, LV_PART_MAIN); /* Channel mixer rows */ for (int i = 0; i < NUM_CHANNELS; i++) { int y_base = 62 + (i * 38); /* Channel name */ channel_labels[i] = lv_label_create(scr); lv_label_set_text(channel_labels[i], channel_names[i]); lv_obj_set_pos(channel_labels[i], 8, y_base + 2); /* Volume bar background */ bar_bg[i] = lv_obj_create(scr); lv_obj_set_size(bar_bg[i], 144, 14); lv_obj_set_pos(bar_bg[i], 140, y_base + 2); lv_obj_set_style_bg_color(bar_bg[i], lv_color_make(33, 4, 4), LV_PART_MAIN); lv_obj_set_style_border_width(bar_bg[i], 1, LV_PART_MAIN); lv_obj_set_style_border_color(bar_bg[i], lv_color_make(50, 55, 70), LV_PART_MAIN); lv_obj_set_style_pad_all(bar_bg[i], 0, LV_PART_MAIN); /* Volume bar fill */ bar_fill[i] = lv_obj_create(bar_bg[i]); lv_obj_set_size(bar_fill[i], 100, 14); lv_obj_set_pos(bar_fill[i], 0, 0); lv_obj_set_style_bg_color(bar_fill[i], lv_color_make(0, 224, 0), LV_PART_MAIN); lv_obj_set_style_border_width(bar_fill[i], 0, LV_PART_MAIN); lv_obj_set_style_pad_all(bar_fill[i], 0, LV_PART_MAIN); /* Volume percentage */ vol_labels[i] = lv_label_create(scr); lv_obj_set_pos(vol_labels[i], 290, y_base + 2); lv_obj_set_style_text_color(vol_labels[i], COLOR_MUTED, LV_PART_MAIN); lv_obj_set_style_text_font(vol_labels[i], &lv_font_montserrat_10, LV_PART_MAIN); } /* Mute warning label */ mute_label = lv_label_create(scr); lv_obj_align(mute_label, LV_ALIGN_BOTTOM_MID, 0, -28); lv_obj_set_style_text_font(mute_label, &lv_font_montserrat_10, LV_PART_MAIN); /* Help text */ lv_obj_t *help = lv_label_create(scr); lv_label_set_text(help, "[NEXT/PREV] Track [WHEEL] Vol [PLAY] Mute"); lv_obj_align(help, LV_ALIGN_BOTTOM_MID, 0, -8); lv_obj_set_style_text_color(help, COLOR_MUTED, LV_PART_MAIN); lv_obj_set_style_text_font(help, &lv_font_montserrat_10, LV_PART_MAIN); /* Open HID consumer control and input */ int hid_fd = open(HID_DEV, O_WRONLY | O_NONBLOCK); int input_fd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK); if (input_fd < 0) { LOG_ERR("cannot open input device"); if (hid_fd >= 0) close(hid_fd); return 1; } if (hid_fd < 0) { LOG_WRN("cannot open %s -- media commands disabled", HID_DEV); } render_mixer(); /* Input loop */ struct input_event ev; while (1) { lv_timer_handler(); if (input_fd >= 0 && read(input_fd, &ev, sizeof(struct input_event)) > 0) { if (ev.type == EV_REL && ev.code == H2_ROTARY_REL) { if (ev.value > 0) { if (channel_volumes[active_channel] < 100) channel_volumes[active_channel] += 5; if (channel_volumes[active_channel] > 100) channel_volumes[active_channel] = 100; /* Send volume up to host */ if (hid_fd >= 0) send_media_command(hid_fd, 0xE9); render_mixer(); } else if (ev.value < 0) { if (channel_volumes[active_channel] > 0) channel_volumes[active_channel] -= 5; if (channel_volumes[active_channel] < 0) channel_volumes[active_channel] = 0; /* Send volume down to host */ if (hid_fd >= 0) send_media_command(hid_fd, 0xEA); render_mixer(); } } else if (ev.type == EV_KEY && ev.value == 1) { if (ev.code == H2_KEY_NEXT) { /* NEXT key cycles channel forward */ active_channel = (AudioChannel)((active_channel + 1) % NUM_CHANNELS); LOG_INF("active channel: %s", channel_names[active_channel]); render_mixer(); } else if (ev.code == H2_KEY_PREV) { /* PREV key cycles channel backward */ active_channel = (AudioChannel)((active_channel - 1 + NUM_CHANNELS) % NUM_CHANNELS); LOG_INF("active channel: %s", channel_names[active_channel]); render_mixer(); } else if (ev.code == H2_KEY_PLAY) { /* PLAY toggles mute */ is_muted = !is_muted; if (hid_fd >= 0) send_media_command(hid_fd, 0xE2); /* MUTE */ LOG_INF("mute toggled: %s", is_muted ? "ON" : "OFF"); render_mixer(); } else if (ev.code == H2_KEY_BACK) { break; } } } usleep(5000); } if (hid_fd >= 0) close(hid_fd); if (input_fd >= 0) close(input_fd); LOG_INF("studio module exited"); return 0; }