260 lines
9.7 KiB
C
Executable File
260 lines
9.7 KiB
C
Executable File
/*
|
|
* 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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <linux/input.h>
|
|
#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;
|
|
} |