/* * JetKVM Audio Output Server * * Standalone C binary for audio output path: * ALSA Capture (TC358743 HDMI) → Opus Encode → IPC Send → Go Process → WebRTC → Browser * * This replaces the Go subprocess that was running with --audio-output-server flag. */ #include "ipc_protocol.h" #include "audio_common.h" #include #include #include #include #include #include #include // Forward declarations from audio.c extern int jetkvm_audio_capture_init(void); extern void jetkvm_audio_capture_close(void); extern int jetkvm_audio_read_encode(void *opus_buf); extern void update_audio_constants(int bitrate, int complexity, int vbr, int vbr_constraint, int signal_type, int bandwidth, int dtx, int lsb_depth, int sr, int ch, int fs, int max_pkt, int sleep_us, int max_attempts, int max_backoff); extern void set_trace_logging(int enabled); extern int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_constraint, int signal_type, int bandwidth, int dtx); // ============================================================================ // GLOBAL STATE // ============================================================================ static volatile sig_atomic_t g_running = 1; // Shutdown flag // Audio configuration (from environment variables) typedef struct { const char *alsa_device; // ALSA capture device (default: "hw:0,0") int opus_bitrate; // Opus bitrate (default: 96000) int opus_complexity; // Opus complexity 0-10 (default: 1) int opus_vbr; // VBR enabled (default: 1) int opus_vbr_constraint; // VBR constraint (default: 1) int opus_signal_type; // Signal type (default: -1000 = auto) int opus_bandwidth; // Bandwidth (default: 1103 = wideband) int opus_dtx; // DTX enabled (default: 0) int opus_lsb_depth; // LSB depth (default: 16) int sample_rate; // Sample rate (default: 48000) int channels; // Channels (default: 2) int frame_size; // Frame size in samples (default: 960) int trace_logging; // Enable trace logging (default: 0) } audio_config_t; // ============================================================================ // CONFIGURATION PARSING // ============================================================================ static void load_audio_config(audio_config_t *config) { // ALSA device configuration config->alsa_device = audio_common_parse_env_string("ALSA_CAPTURE_DEVICE", "hw:0,0"); // Opus encoder configuration config->opus_bitrate = audio_common_parse_env_int("OPUS_BITRATE", 96000); config->opus_complexity = audio_common_parse_env_int("OPUS_COMPLEXITY", 1); config->opus_vbr = audio_common_parse_env_int("OPUS_VBR", 1); config->opus_vbr_constraint = audio_common_parse_env_int("OPUS_VBR_CONSTRAINT", 1); config->opus_signal_type = audio_common_parse_env_int("OPUS_SIGNAL_TYPE", -1000); config->opus_bandwidth = audio_common_parse_env_int("OPUS_BANDWIDTH", 1103); config->opus_dtx = audio_common_parse_env_int("OPUS_DTX", 0); config->opus_lsb_depth = audio_common_parse_env_int("OPUS_LSB_DEPTH", 16); // Audio format config->sample_rate = audio_common_parse_env_int("AUDIO_SAMPLE_RATE", 48000); config->channels = audio_common_parse_env_int("AUDIO_CHANNELS", 2); config->frame_size = audio_common_parse_env_int("AUDIO_FRAME_SIZE", 960); // Logging config->trace_logging = audio_common_is_trace_enabled(); // Log configuration printf("Audio Output Server Configuration:\n"); printf(" ALSA Device: %s\n", config->alsa_device); printf(" Sample Rate: %d Hz\n", config->sample_rate); printf(" Channels: %d\n", config->channels); printf(" Frame Size: %d samples\n", config->frame_size); printf(" Opus Bitrate: %d bps\n", config->opus_bitrate); printf(" Opus Complexity: %d\n", config->opus_complexity); printf(" Trace Logging: %s\n", config->trace_logging ? "enabled" : "disabled"); } // ============================================================================ // MESSAGE HANDLING // ============================================================================ /** * Handle OpusConfig message: update encoder parameters dynamically. * Returns 0 on success, -1 on error. */ static int handle_opus_config(const uint8_t *data, uint32_t length) { ipc_opus_config_t config; if (ipc_parse_opus_config(data, length, &config) != 0) { fprintf(stderr, "Failed to parse Opus config\n"); return -1; } printf("Received Opus config: bitrate=%u, complexity=%u, vbr=%u\n", config.bitrate, config.complexity, config.vbr); // Apply configuration to encoder // Note: Signal type needs special handling for negative values int signal_type = (int)(int32_t)config.signal_type; // Treat as signed int result = update_opus_encoder_params( config.bitrate, config.complexity, config.vbr, config.vbr, // Use VBR value for constraint (simplified) signal_type, config.bandwidth, config.dtx ); if (result != 0) { fprintf(stderr, "Warning: Failed to apply some Opus encoder parameters\n"); // Continue anyway - encoder may not be initialized yet } return 0; } /** * Handle incoming IPC messages from client (non-blocking). * Returns 0 on success, -1 on error. */ static int handle_incoming_messages(int client_sock) { // Set non-blocking mode for client socket int flags = fcntl(client_sock, F_GETFL, 0); fcntl(client_sock, F_SETFL, flags | O_NONBLOCK); ipc_message_t msg; // Try to read message (non-blocking) int result = ipc_read_message(client_sock, &msg, IPC_MAGIC_OUTPUT); // Restore blocking mode fcntl(client_sock, F_SETFL, flags); if (result != 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { return 0; // No message available, not an error } return -1; // Connection error } // Process message based on type switch (msg.header.type) { case IPC_MSG_TYPE_OPUS_CONFIG: handle_opus_config(msg.data, msg.header.length); break; case IPC_MSG_TYPE_STOP: printf("Received stop message\n"); g_running = 0; break; case IPC_MSG_TYPE_HEARTBEAT: // Informational only, no response needed break; default: printf("Warning: Unknown message type: %u\n", msg.header.type); break; } ipc_free_message(&msg); return 0; } // ============================================================================ // MAIN LOOP // ============================================================================ /** * Main audio capture and encode loop. * Continuously reads from ALSA, encodes to Opus, sends via IPC. */ static int run_audio_loop(int client_sock) { uint8_t opus_buffer[IPC_MAX_FRAME_SIZE]; int consecutive_errors = 0; const int max_consecutive_errors = 10; int frame_count = 0; printf("Starting audio output loop...\n"); while (g_running) { // Handle any incoming configuration messages (non-blocking) if (handle_incoming_messages(client_sock) < 0) { fprintf(stderr, "Client disconnected, waiting for reconnection...\n"); break; // Client disconnected } // Capture audio and encode to Opus int opus_size = jetkvm_audio_read_encode(opus_buffer); if (opus_size < 0) { consecutive_errors++; fprintf(stderr, "Audio read/encode failed (error %d/%d)\n", consecutive_errors, max_consecutive_errors); if (consecutive_errors >= max_consecutive_errors) { fprintf(stderr, "Too many consecutive errors, giving up\n"); return -1; } usleep(10000); // 10ms backoff continue; } if (opus_size == 0) { // No data available (non-blocking mode or empty frame) usleep(1000); // 1ms sleep continue; } // Reset error counter on success consecutive_errors = 0; frame_count++; // Send Opus frame via IPC if (ipc_write_message(client_sock, IPC_MAGIC_OUTPUT, IPC_MSG_TYPE_OPUS_FRAME, opus_buffer, opus_size) != 0) { fprintf(stderr, "Failed to send frame to client\n"); break; // Client disconnected } // Trace logging (periodic) if (frame_count % 1000 == 1) { printf("Sent frame %d (size=%d bytes)\n", frame_count, opus_size); } // Small delay to prevent busy-waiting (frame rate ~50 FPS @ 48kHz/960) usleep(1000); // 1ms } printf("Audio output loop ended after %d frames\n", frame_count); return 0; } // ============================================================================ // MAIN // ============================================================================ int main(int argc, char **argv) { printf("JetKVM Audio Output Server Starting...\n"); // Setup signal handlers audio_common_setup_signal_handlers(&g_running); // Load configuration from environment audio_config_t config; load_audio_config(&config); // Set trace logging set_trace_logging(config.trace_logging); // Apply audio constants to audio.c update_audio_constants( config.opus_bitrate, config.opus_complexity, config.opus_vbr, config.opus_vbr_constraint, config.opus_signal_type, config.opus_bandwidth, config.opus_dtx, config.opus_lsb_depth, config.sample_rate, config.channels, config.frame_size, 1500, // max_packet_size 1000, // sleep_microseconds 5, // max_attempts 500000 // max_backoff_us ); // Initialize audio capture printf("Initializing audio capture on device: %s\n", config.alsa_device); if (jetkvm_audio_capture_init() != 0) { fprintf(stderr, "Failed to initialize audio capture\n"); return 1; } // Create IPC server int server_sock = ipc_create_server(IPC_SOCKET_OUTPUT); if (server_sock < 0) { fprintf(stderr, "Failed to create IPC server\n"); jetkvm_audio_capture_close(); return 1; } // Main connection loop while (g_running) { printf("Waiting for client connection...\n"); int client_sock = ipc_accept_client(server_sock); if (client_sock < 0) { if (g_running) { fprintf(stderr, "Failed to accept client, retrying...\n"); sleep(1); continue; } break; // Shutting down } // Run audio loop with this client run_audio_loop(client_sock); // Close client connection close(client_sock); if (g_running) { printf("Client disconnected, waiting for next client...\n"); } } // Cleanup printf("Shutting down audio output server...\n"); close(server_sock); unlink(IPC_SOCKET_OUTPUT); jetkvm_audio_capture_close(); printf("Audio output server exited cleanly\n"); return 0; }