mirror of https://github.com/jetkvm/kvm.git
[WIP] Maintainability Improvement: Add debug logging throughout the audio system for easy debugging and troubleshooting
This commit is contained in:
parent
0944c886e5
commit
1f88dab95f
|
@ -409,7 +409,7 @@ npm install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Enable debug logging
|
# Enable debug logging
|
||||||
export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc"
|
export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc,audio"
|
||||||
|
|
||||||
# Frontend development
|
# Frontend development
|
||||||
export JETKVM_PROXY_URL="ws://<IP>"
|
export JETKVM_PROXY_URL="ws://<IP>"
|
||||||
|
@ -461,7 +461,7 @@ curl http://api:$JETKVM_PASSWORD@YOUR_DEVICE_IP/developer/pprof/
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Enable trace logging (useful for debugging)
|
# Enable trace logging (useful for debugging)
|
||||||
export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc"
|
export LOG_TRACE_SCOPES="jetkvm,cloud,websocket,native,jsonrpc,audio"
|
||||||
|
|
||||||
# For frontend development
|
# For frontend development
|
||||||
export JETKVM_PROXY_URL="ws://<JETKVM_IP>"
|
export JETKVM_PROXY_URL="ws://<JETKVM_IP>"
|
||||||
|
|
|
@ -41,7 +41,7 @@ REMOTE_USER="root"
|
||||||
REMOTE_PATH="/userdata/jetkvm/bin"
|
REMOTE_PATH="/userdata/jetkvm/bin"
|
||||||
SKIP_UI_BUILD=false
|
SKIP_UI_BUILD=false
|
||||||
RESET_USB_HID_DEVICE=false
|
RESET_USB_HID_DEVICE=false
|
||||||
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
|
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc,audio}"
|
||||||
RUN_GO_TESTS=false
|
RUN_GO_TESTS=false
|
||||||
RUN_GO_TESTS_ONLY=false
|
RUN_GO_TESTS_ONLY=false
|
||||||
INSTALL_APP=false
|
INSTALL_APP=false
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
|
/*
|
||||||
|
* JetKVM Audio Processing Module
|
||||||
|
*
|
||||||
|
* This module handles bidirectional audio processing for JetKVM:
|
||||||
|
* - Audio INPUT: Client microphone → Device speakers (decode Opus → ALSA playback)
|
||||||
|
* - Audio OUTPUT: Device microphone → Client speakers (ALSA capture → encode Opus)
|
||||||
|
*/
|
||||||
|
|
||||||
#include <alsa/asoundlib.h>
|
#include <alsa/asoundlib.h>
|
||||||
#include <opus.h>
|
#include <opus.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
@ -6,39 +14,74 @@
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
||||||
// C state for ALSA/Opus with safety flags
|
// ============================================================================
|
||||||
static snd_pcm_t *pcm_capture_handle = NULL;
|
// GLOBAL STATE VARIABLES
|
||||||
static snd_pcm_t *pcm_playback_handle = NULL;
|
// ============================================================================
|
||||||
static OpusEncoder *encoder = NULL;
|
|
||||||
static OpusDecoder *decoder = NULL;
|
|
||||||
// Opus encoder settings - initialized from Go configuration
|
|
||||||
static int opus_bitrate = 96000; // Will be set from Config.CGOOpusBitrate
|
|
||||||
static int opus_complexity = 3; // Will be set from Config.CGOOpusComplexity
|
|
||||||
static int opus_vbr = 1; // Will be set from Config.CGOOpusVBR
|
|
||||||
static int opus_vbr_constraint = 1; // Will be set from Config.CGOOpusVBRConstraint
|
|
||||||
static int opus_signal_type = 3; // Will be set from Config.CGOOpusSignalType
|
|
||||||
static int opus_bandwidth = 1105; // OPUS_BANDWIDTH_WIDEBAND for compatibility (was 1101)
|
|
||||||
static int opus_dtx = 0; // Will be set from Config.CGOOpusDTX
|
|
||||||
static int opus_lsb_depth = 16; // LSB depth for improved bit allocation on constrained hardware
|
|
||||||
static int sample_rate = 48000; // Will be set from Config.CGOSampleRate
|
|
||||||
static int channels = 2; // Will be set from Config.CGOChannels
|
|
||||||
static int frame_size = 960; // Will be set from Config.CGOFrameSize
|
|
||||||
static int max_packet_size = 1500; // Will be set from Config.CGOMaxPacketSize
|
|
||||||
static int sleep_microseconds = 1000; // Will be set from Config.CGOUsleepMicroseconds
|
|
||||||
static int max_attempts_global = 5; // Will be set from Config.CGOMaxAttempts
|
|
||||||
static int max_backoff_us_global = 500000; // Will be set from Config.CGOMaxBackoffMicroseconds
|
|
||||||
// Hardware optimization flags for constrained environments
|
|
||||||
static int optimized_buffer_size = 1; // Disable optimized buffer sizing for stability (was 1)
|
|
||||||
|
|
||||||
// C function declarations (implementations are below)
|
// ALSA device handles
|
||||||
int jetkvm_audio_capture_init();
|
static snd_pcm_t *pcm_capture_handle = NULL; // Device microphone (OUTPUT path)
|
||||||
void jetkvm_audio_capture_close();
|
static snd_pcm_t *pcm_playback_handle = NULL; // Device speakers (INPUT path)
|
||||||
int jetkvm_audio_read_encode(void *opus_buf);
|
|
||||||
int jetkvm_audio_decode_write(void *opus_buf, int opus_size);
|
|
||||||
int jetkvm_audio_playback_init();
|
|
||||||
void jetkvm_audio_playback_close();
|
|
||||||
|
|
||||||
// Function to update constants from Go configuration
|
// Opus codec instances
|
||||||
|
static OpusEncoder *encoder = NULL; // For OUTPUT path (device mic → client)
|
||||||
|
static OpusDecoder *decoder = NULL; // For INPUT path (client → device speakers)
|
||||||
|
// Audio format configuration
|
||||||
|
static int sample_rate = 48000; // Sample rate in Hz
|
||||||
|
static int channels = 2; // Number of audio channels (stereo)
|
||||||
|
static int frame_size = 960; // Frames per Opus packet
|
||||||
|
|
||||||
|
// Opus encoder configuration
|
||||||
|
static int opus_bitrate = 96000; // Bitrate in bits/second
|
||||||
|
static int opus_complexity = 3; // Encoder complexity (0-10)
|
||||||
|
static int opus_vbr = 1; // Variable bitrate enabled
|
||||||
|
static int opus_vbr_constraint = 1; // Constrained VBR
|
||||||
|
static int opus_signal_type = 3; // Audio signal type
|
||||||
|
static int opus_bandwidth = 1105; // Bandwidth setting
|
||||||
|
static int opus_dtx = 0; // Discontinuous transmission
|
||||||
|
static int opus_lsb_depth = 16; // LSB depth for bit allocation
|
||||||
|
|
||||||
|
// Network and buffer configuration
|
||||||
|
static int max_packet_size = 1500; // Maximum Opus packet size
|
||||||
|
|
||||||
|
// Error handling and retry configuration
|
||||||
|
static int sleep_microseconds = 1000; // Base sleep time for retries
|
||||||
|
static int max_attempts_global = 5; // Maximum retry attempts
|
||||||
|
static int max_backoff_us_global = 500000; // Maximum backoff time
|
||||||
|
|
||||||
|
// Performance optimization flags
|
||||||
|
static int optimized_buffer_size = 1; // Use optimized buffer sizing
|
||||||
|
static int trace_logging_enabled = 0; // Enable detailed trace logging
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// FUNCTION DECLARATIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Audio OUTPUT path functions (device microphone → client speakers)
|
||||||
|
int jetkvm_audio_capture_init(); // Initialize capture device and Opus encoder
|
||||||
|
void jetkvm_audio_capture_close(); // Cleanup capture resources
|
||||||
|
int jetkvm_audio_read_encode(void *opus_buf); // Read PCM, encode to Opus
|
||||||
|
|
||||||
|
// Audio INPUT path functions (client microphone → device speakers)
|
||||||
|
int jetkvm_audio_playback_init(); // Initialize playback device and Opus decoder
|
||||||
|
void jetkvm_audio_playback_close(); // Cleanup playback resources
|
||||||
|
int jetkvm_audio_decode_write(void *opus_buf, int opus_size); // Decode Opus, write PCM
|
||||||
|
|
||||||
|
// Configuration and utility functions
|
||||||
|
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);
|
||||||
|
void set_trace_logging(int enabled);
|
||||||
|
int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_constraint,
|
||||||
|
int signal_type, int bandwidth, int dtx);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CONFIGURATION FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update audio configuration constants from Go
|
||||||
|
* Called during initialization to sync C variables with Go config
|
||||||
|
*/
|
||||||
void update_audio_constants(int bitrate, int complexity, int vbr, int vbr_constraint,
|
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 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) {
|
int fs, int max_pkt, int sleep_us, int max_attempts, int max_backoff) {
|
||||||
|
@ -59,22 +102,38 @@ void update_audio_constants(int bitrate, int complexity, int vbr, int vbr_constr
|
||||||
max_backoff_us_global = max_backoff;
|
max_backoff_us_global = max_backoff;
|
||||||
}
|
}
|
||||||
|
|
||||||
// State tracking to prevent race conditions during rapid start/stop
|
/**
|
||||||
static volatile int capture_initializing = 0;
|
* Enable or disable trace logging
|
||||||
static volatile int capture_initialized = 0;
|
* When enabled, detailed debug information is printed to stdout
|
||||||
static volatile int playback_initializing = 0;
|
* Zero overhead when disabled - no function calls or string formatting occur
|
||||||
static volatile int playback_initialized = 0;
|
*/
|
||||||
|
void set_trace_logging(int enabled) {
|
||||||
// Function to dynamically update Opus encoder parameters
|
trace_logging_enabled = enabled;
|
||||||
int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_constraint,
|
|
||||||
int signal_type, int bandwidth, int dtx) {
|
|
||||||
// This function updates encoder parameters for audio input (capture)
|
|
||||||
// Only capture uses the encoder; playback uses a separate decoder
|
|
||||||
if (!encoder || !capture_initialized) {
|
|
||||||
return -1; // Audio encoder not initialized
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the static variables
|
// ============================================================================
|
||||||
|
// INITIALIZATION STATE TRACKING
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Thread-safe initialization state tracking to prevent race conditions
|
||||||
|
static volatile int capture_initializing = 0; // OUTPUT path init in progress
|
||||||
|
static volatile int capture_initialized = 0; // OUTPUT path ready
|
||||||
|
static volatile int playback_initializing = 0; // INPUT path init in progress
|
||||||
|
static volatile int playback_initialized = 0; // INPUT path ready
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Opus encoder parameters dynamically
|
||||||
|
* Used for OUTPUT path (device microphone → client speakers)
|
||||||
|
*
|
||||||
|
* @return 0 on success, -1 if encoder not initialized, >0 if some settings failed
|
||||||
|
*/
|
||||||
|
int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_constraint,
|
||||||
|
int signal_type, int bandwidth, int dtx) {
|
||||||
|
if (!encoder || !capture_initialized) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local configuration
|
||||||
opus_bitrate = bitrate;
|
opus_bitrate = bitrate;
|
||||||
opus_complexity = complexity;
|
opus_complexity = complexity;
|
||||||
opus_vbr = vbr;
|
opus_vbr = vbr;
|
||||||
|
@ -83,7 +142,7 @@ int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_con
|
||||||
opus_bandwidth = bandwidth;
|
opus_bandwidth = bandwidth;
|
||||||
opus_dtx = dtx;
|
opus_dtx = dtx;
|
||||||
|
|
||||||
// Apply the new settings to the encoder
|
// Apply settings to Opus encoder
|
||||||
int result = 0;
|
int result = 0;
|
||||||
result |= opus_encoder_ctl(encoder, OPUS_SET_BITRATE(opus_bitrate));
|
result |= opus_encoder_ctl(encoder, OPUS_SET_BITRATE(opus_bitrate));
|
||||||
result |= opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(opus_complexity));
|
result |= opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(opus_complexity));
|
||||||
|
@ -93,10 +152,22 @@ int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_con
|
||||||
result |= opus_encoder_ctl(encoder, OPUS_SET_BANDWIDTH(opus_bandwidth));
|
result |= opus_encoder_ctl(encoder, OPUS_SET_BANDWIDTH(opus_bandwidth));
|
||||||
result |= opus_encoder_ctl(encoder, OPUS_SET_DTX(opus_dtx));
|
result |= opus_encoder_ctl(encoder, OPUS_SET_DTX(opus_dtx));
|
||||||
|
|
||||||
return result; // 0 on success, non-zero on error
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced ALSA device opening with exponential backoff retry logic
|
// ============================================================================
|
||||||
|
// ALSA UTILITY FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely open ALSA device with exponential backoff retry logic
|
||||||
|
* Handles common device busy/unavailable scenarios with appropriate retry strategies
|
||||||
|
*
|
||||||
|
* @param handle Pointer to PCM handle to be set
|
||||||
|
* @param device ALSA device name (e.g., "hw:1,0")
|
||||||
|
* @param stream Stream direction (capture or playback)
|
||||||
|
* @return 0 on success, negative error code on failure
|
||||||
|
*/
|
||||||
static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream_t stream) {
|
static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream_t stream) {
|
||||||
int attempt = 0;
|
int attempt = 0;
|
||||||
int err;
|
int err;
|
||||||
|
@ -134,7 +205,14 @@ static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimized ALSA configuration with stack allocation and performance tuning
|
/**
|
||||||
|
* Configure ALSA device with optimized parameters
|
||||||
|
* Sets up hardware and software parameters for optimal performance on constrained hardware
|
||||||
|
*
|
||||||
|
* @param handle ALSA PCM handle
|
||||||
|
* @param device_name Device name for debugging (not used in current implementation)
|
||||||
|
* @return 0 on success, negative error code on failure
|
||||||
|
*/
|
||||||
static int configure_alsa_device(snd_pcm_t *handle, const char *device_name) {
|
static int configure_alsa_device(snd_pcm_t *handle, const char *device_name) {
|
||||||
snd_pcm_hw_params_t *params;
|
snd_pcm_hw_params_t *params;
|
||||||
snd_pcm_sw_params_t *sw_params;
|
snd_pcm_sw_params_t *sw_params;
|
||||||
|
@ -212,7 +290,22 @@ static int configure_alsa_device(snd_pcm_t *handle, const char *device_name) {
|
||||||
return snd_pcm_prepare(handle);
|
return snd_pcm_prepare(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize ALSA and Opus encoder with improved safety
|
// ============================================================================
|
||||||
|
// AUDIO OUTPUT PATH FUNCTIONS (Device Microphone → Client Speakers)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize audio OUTPUT path: device microphone capture and Opus encoder
|
||||||
|
* This enables sending device audio to the client
|
||||||
|
*
|
||||||
|
* Thread-safe with atomic operations to prevent concurrent initialization
|
||||||
|
*
|
||||||
|
* @return 0 on success, negative error codes on failure:
|
||||||
|
* -EBUSY: Already initializing
|
||||||
|
* -1: ALSA device open failed
|
||||||
|
* -2: ALSA device configuration failed
|
||||||
|
* -3: Opus encoder creation failed
|
||||||
|
*/
|
||||||
int jetkvm_audio_capture_init() {
|
int jetkvm_audio_capture_init() {
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
|
@ -282,9 +375,26 @@ int jetkvm_audio_capture_init() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// jetkvm_audio_read_encode captures audio from ALSA, encodes with Opus, and handles errors.
|
/**
|
||||||
// Implements robust error recovery for buffer underruns and device suspension.
|
* Capture audio from device microphone and encode to Opus (OUTPUT path)
|
||||||
// Returns: >0 (bytes written), -1 (init error), -2 (unrecoverable error)
|
*
|
||||||
|
* This function:
|
||||||
|
* 1. Reads PCM audio from device microphone via ALSA
|
||||||
|
* 2. Handles ALSA errors with robust recovery strategies
|
||||||
|
* 3. Encodes PCM to Opus format for network transmission
|
||||||
|
* 4. Provides zero-overhead trace logging when enabled
|
||||||
|
*
|
||||||
|
* Error recovery includes handling:
|
||||||
|
* - Buffer underruns (-EPIPE)
|
||||||
|
* - Device suspension (-ESTRPIPE)
|
||||||
|
* - I/O errors (-EIO)
|
||||||
|
* - Device busy conditions (-EBUSY, -EAGAIN)
|
||||||
|
*
|
||||||
|
* @param opus_buf Buffer to store encoded Opus data (must be at least max_packet_size)
|
||||||
|
* @return >0: Number of Opus bytes written
|
||||||
|
* 0: No audio data available (not an error)
|
||||||
|
* -1: Initialization error or unrecoverable failure
|
||||||
|
*/
|
||||||
int jetkvm_audio_read_encode(void *opus_buf) {
|
int jetkvm_audio_read_encode(void *opus_buf) {
|
||||||
short pcm_buffer[1920]; // max 2ch*960
|
short pcm_buffer[1920]; // max 2ch*960
|
||||||
unsigned char *out = (unsigned char*)opus_buf;
|
unsigned char *out = (unsigned char*)opus_buf;
|
||||||
|
@ -294,6 +404,10 @@ int jetkvm_audio_read_encode(void *opus_buf) {
|
||||||
|
|
||||||
// Safety checks
|
// Safety checks
|
||||||
if (!capture_initialized || !pcm_capture_handle || !encoder || !opus_buf) {
|
if (!capture_initialized || !pcm_capture_handle || !encoder || !opus_buf) {
|
||||||
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_OUTPUT] jetkvm_audio_read_encode: Failed safety checks - capture_initialized=%d, pcm_capture_handle=%p, encoder=%p, opus_buf=%p\n",
|
||||||
|
capture_initialized, pcm_capture_handle, encoder, opus_buf);
|
||||||
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,10 +497,29 @@ retry_read:
|
||||||
}
|
}
|
||||||
|
|
||||||
int nb_bytes = opus_encode(encoder, pcm_buffer, frame_size, out, max_packet_size);
|
int nb_bytes = opus_encode(encoder, pcm_buffer, frame_size, out, max_packet_size);
|
||||||
|
|
||||||
|
if (trace_logging_enabled && nb_bytes > 0) {
|
||||||
|
printf("[AUDIO_OUTPUT] jetkvm_audio_read_encode: Successfully encoded %d PCM frames to %d Opus bytes\n", pcm_rc, nb_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
return nb_bytes;
|
return nb_bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize ALSA playback with improved safety
|
// ============================================================================
|
||||||
|
// AUDIO INPUT PATH FUNCTIONS (Client Microphone → Device Speakers)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize audio INPUT path: ALSA playback device and Opus decoder
|
||||||
|
* This enables playing client audio through device speakers
|
||||||
|
*
|
||||||
|
* Thread-safe with atomic operations to prevent concurrent initialization
|
||||||
|
*
|
||||||
|
* @return 0 on success, negative error codes on failure:
|
||||||
|
* -EBUSY: Already initializing
|
||||||
|
* -1: ALSA device open failed or configuration failed
|
||||||
|
* -2: Opus decoder creation failed
|
||||||
|
*/
|
||||||
int jetkvm_audio_playback_init() {
|
int jetkvm_audio_playback_init() {
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
|
@ -446,8 +579,30 @@ int jetkvm_audio_playback_init() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// jetkvm_audio_decode_write decodes Opus data and writes PCM to ALSA playback device
|
/**
|
||||||
// with error recovery and packet loss concealment
|
* Decode Opus audio and play through device speakers (INPUT path)
|
||||||
|
*
|
||||||
|
* This function:
|
||||||
|
* 1. Validates input parameters and Opus packet size
|
||||||
|
* 2. Decodes Opus data to PCM format
|
||||||
|
* 3. Implements packet loss concealment for network issues
|
||||||
|
* 4. Writes PCM to device speakers via ALSA
|
||||||
|
* 5. Handles ALSA playback errors with recovery strategies
|
||||||
|
* 6. Provides zero-overhead trace logging when enabled
|
||||||
|
*
|
||||||
|
* Error recovery includes handling:
|
||||||
|
* - Buffer underruns (-EPIPE) with progressive recovery
|
||||||
|
* - Device suspension (-ESTRPIPE) with resume logic
|
||||||
|
* - I/O errors (-EIO) with device reset
|
||||||
|
* - Device not ready (-EAGAIN) with retry logic
|
||||||
|
*
|
||||||
|
* @param opus_buf Buffer containing Opus-encoded audio data
|
||||||
|
* @param opus_size Size of Opus data in bytes
|
||||||
|
* @return >0: Number of PCM frames written to speakers
|
||||||
|
* 0: Frame skipped (not an error)
|
||||||
|
* -1: Invalid input or decode failure
|
||||||
|
* -2: Unrecoverable ALSA error
|
||||||
|
*/
|
||||||
int jetkvm_audio_decode_write(void *opus_buf, int opus_size) {
|
int jetkvm_audio_decode_write(void *opus_buf, int opus_size) {
|
||||||
short pcm_buffer[1920]; // max 2ch*960
|
short pcm_buffer[1920]; // max 2ch*960
|
||||||
unsigned char *in = (unsigned char*)opus_buf;
|
unsigned char *in = (unsigned char*)opus_buf;
|
||||||
|
@ -457,22 +612,44 @@ int jetkvm_audio_decode_write(void *opus_buf, int opus_size) {
|
||||||
|
|
||||||
// Safety checks
|
// Safety checks
|
||||||
if (!playback_initialized || !pcm_playback_handle || !decoder || !opus_buf || opus_size <= 0) {
|
if (!playback_initialized || !pcm_playback_handle || !decoder || !opus_buf || opus_size <= 0) {
|
||||||
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Failed safety checks - playback_initialized=%d, pcm_playback_handle=%p, decoder=%p, opus_buf=%p, opus_size=%d\n",
|
||||||
|
playback_initialized, pcm_playback_handle, decoder, opus_buf, opus_size);
|
||||||
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional bounds checking
|
// Additional bounds checking
|
||||||
if (opus_size > max_packet_size) {
|
if (opus_size > max_packet_size) {
|
||||||
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Opus packet too large - size=%d, max=%d\n", opus_size, max_packet_size);
|
||||||
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Processing Opus packet - size=%d bytes\n", opus_size);
|
||||||
|
}
|
||||||
|
|
||||||
// Decode Opus to PCM with error handling
|
// Decode Opus to PCM with error handling
|
||||||
int pcm_frames = opus_decode(decoder, in, opus_size, pcm_buffer, frame_size, 0);
|
int pcm_frames = opus_decode(decoder, in, opus_size, pcm_buffer, frame_size, 0);
|
||||||
if (pcm_frames < 0) {
|
if (pcm_frames < 0) {
|
||||||
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Opus decode failed with error %d, attempting packet loss concealment\n", pcm_frames);
|
||||||
|
}
|
||||||
// Try packet loss concealment on decode error
|
// Try packet loss concealment on decode error
|
||||||
pcm_frames = opus_decode(decoder, NULL, 0, pcm_buffer, frame_size, 0);
|
pcm_frames = opus_decode(decoder, NULL, 0, pcm_buffer, frame_size, 0);
|
||||||
if (pcm_frames < 0) {
|
if (pcm_frames < 0) {
|
||||||
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Packet loss concealment also failed with error %d\n", pcm_frames);
|
||||||
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Packet loss concealment succeeded, recovered %d frames\n", pcm_frames);
|
||||||
|
}
|
||||||
|
} else if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Opus decode successful - decoded %d PCM frames\n", pcm_frames);
|
||||||
}
|
}
|
||||||
|
|
||||||
retry_write:
|
retry_write:
|
||||||
|
@ -480,36 +657,60 @@ retry_write:
|
||||||
// Write PCM to playback device with robust recovery
|
// Write PCM to playback device with robust recovery
|
||||||
int pcm_rc = snd_pcm_writei(pcm_playback_handle, pcm_buffer, pcm_frames);
|
int pcm_rc = snd_pcm_writei(pcm_playback_handle, pcm_buffer, pcm_frames);
|
||||||
if (pcm_rc < 0) {
|
if (pcm_rc < 0) {
|
||||||
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: ALSA write failed with error %d (%s), attempt %d/%d\n",
|
||||||
|
pcm_rc, snd_strerror(pcm_rc), recovery_attempts + 1, max_recovery_attempts);
|
||||||
|
}
|
||||||
|
|
||||||
if (pcm_rc == -EPIPE) {
|
if (pcm_rc == -EPIPE) {
|
||||||
// Buffer underrun - implement progressive recovery
|
// Buffer underrun - implement progressive recovery
|
||||||
recovery_attempts++;
|
recovery_attempts++;
|
||||||
if (recovery_attempts > max_recovery_attempts) {
|
if (recovery_attempts > max_recovery_attempts) {
|
||||||
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Buffer underrun recovery failed after %d attempts\n", max_recovery_attempts);
|
||||||
|
}
|
||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Buffer underrun detected, attempting recovery (attempt %d)\n", recovery_attempts);
|
||||||
|
}
|
||||||
// Try to recover with prepare
|
// Try to recover with prepare
|
||||||
err = snd_pcm_prepare(pcm_playback_handle);
|
err = snd_pcm_prepare(pcm_playback_handle);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: snd_pcm_prepare failed (%s), trying drop+prepare\n", snd_strerror(err));
|
||||||
|
}
|
||||||
// If prepare fails, try drop and prepare
|
// If prepare fails, try drop and prepare
|
||||||
snd_pcm_drop(pcm_playback_handle);
|
snd_pcm_drop(pcm_playback_handle);
|
||||||
err = snd_pcm_prepare(pcm_playback_handle);
|
err = snd_pcm_prepare(pcm_playback_handle);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: drop+prepare recovery failed (%s)\n", snd_strerror(err));
|
||||||
|
}
|
||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait before retry to allow device to stabilize
|
// Wait before retry to allow device to stabilize
|
||||||
snd_pcm_wait(pcm_playback_handle, sleep_microseconds * recovery_attempts / 1000);
|
snd_pcm_wait(pcm_playback_handle, sleep_microseconds * recovery_attempts / 1000);
|
||||||
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Buffer underrun recovery successful, retrying write\n");
|
||||||
|
}
|
||||||
goto retry_write;
|
goto retry_write;
|
||||||
} else if (pcm_rc == -ESTRPIPE) {
|
} else if (pcm_rc == -ESTRPIPE) {
|
||||||
// Device suspended, implement robust resume logic
|
// Device suspended, implement robust resume logic
|
||||||
recovery_attempts++;
|
recovery_attempts++;
|
||||||
if (recovery_attempts > max_recovery_attempts) {
|
if (recovery_attempts > max_recovery_attempts) {
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: Device suspend recovery failed after %d attempts\n", max_recovery_attempts);
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Device suspend recovery failed after %d attempts\n", max_recovery_attempts);
|
||||||
|
}
|
||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: Device suspended, attempting resume (attempt %d)\n", recovery_attempts);
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Device suspended, attempting resume (attempt %d)\n", recovery_attempts);
|
||||||
|
}
|
||||||
// Try to resume with timeout
|
// Try to resume with timeout
|
||||||
int resume_attempts = 0;
|
int resume_attempts = 0;
|
||||||
while ((err = snd_pcm_resume(pcm_playback_handle)) == -EAGAIN && resume_attempts < 10) {
|
while ((err = snd_pcm_resume(pcm_playback_handle)) == -EAGAIN && resume_attempts < 10) {
|
||||||
|
@ -517,64 +718,98 @@ retry_write:
|
||||||
resume_attempts++;
|
resume_attempts++;
|
||||||
}
|
}
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: Device resume failed (%s), trying prepare fallback\n", snd_strerror(err));
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Device resume failed (%s), trying prepare fallback\n", snd_strerror(err));
|
||||||
|
}
|
||||||
// Resume failed, try prepare as fallback
|
// Resume failed, try prepare as fallback
|
||||||
err = snd_pcm_prepare(pcm_playback_handle);
|
err = snd_pcm_prepare(pcm_playback_handle);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: Prepare fallback failed (%s)\n", snd_strerror(err));
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Prepare fallback failed (%s)\n", snd_strerror(err));
|
||||||
|
}
|
||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Wait before retry to allow device to stabilize
|
// Wait before retry to allow device to stabilize
|
||||||
snd_pcm_wait(pcm_playback_handle, sleep_microseconds * recovery_attempts / 1000);
|
snd_pcm_wait(pcm_playback_handle, sleep_microseconds * recovery_attempts / 1000);
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: Device suspend recovery successful, skipping frame\n");
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Device suspend recovery successful, skipping frame\n");
|
||||||
|
}
|
||||||
return 0; // Skip this frame but don't fail
|
return 0; // Skip this frame but don't fail
|
||||||
} else if (pcm_rc == -ENODEV) {
|
} else if (pcm_rc == -ENODEV) {
|
||||||
// Device disconnected - critical error
|
// Device disconnected - critical error
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: Device disconnected (ENODEV) - critical error\n");
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Device disconnected (ENODEV) - critical error\n");
|
||||||
|
}
|
||||||
return -2;
|
return -2;
|
||||||
} else if (pcm_rc == -EIO) {
|
} else if (pcm_rc == -EIO) {
|
||||||
// I/O error - try recovery once
|
// I/O error - try recovery once
|
||||||
recovery_attempts++;
|
recovery_attempts++;
|
||||||
if (recovery_attempts <= max_recovery_attempts) {
|
if (recovery_attempts <= max_recovery_attempts) {
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: I/O error detected, attempting recovery\n");
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: I/O error detected, attempting recovery\n");
|
||||||
|
}
|
||||||
snd_pcm_drop(pcm_playback_handle);
|
snd_pcm_drop(pcm_playback_handle);
|
||||||
err = snd_pcm_prepare(pcm_playback_handle);
|
err = snd_pcm_prepare(pcm_playback_handle);
|
||||||
if (err >= 0) {
|
if (err >= 0) {
|
||||||
snd_pcm_wait(pcm_playback_handle, sleep_microseconds / 1000);
|
snd_pcm_wait(pcm_playback_handle, sleep_microseconds / 1000);
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: I/O error recovery successful, retrying write\n");
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: I/O error recovery successful, retrying write\n");
|
||||||
|
}
|
||||||
goto retry_write;
|
goto retry_write;
|
||||||
}
|
}
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: I/O error recovery failed (%s)\n", snd_strerror(err));
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: I/O error recovery failed (%s)\n", snd_strerror(err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return -2;
|
return -2;
|
||||||
} else if (pcm_rc == -EAGAIN) {
|
} else if (pcm_rc == -EAGAIN) {
|
||||||
// Device not ready - brief wait and retry
|
// Device not ready - brief wait and retry
|
||||||
recovery_attempts++;
|
recovery_attempts++;
|
||||||
if (recovery_attempts <= max_recovery_attempts) {
|
if (recovery_attempts <= max_recovery_attempts) {
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: Device not ready (EAGAIN), waiting and retrying\n");
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Device not ready (EAGAIN), waiting and retrying\n");
|
||||||
|
}
|
||||||
usleep(sleep_microseconds / 4);
|
usleep(sleep_microseconds / 4);
|
||||||
goto retry_write;
|
goto retry_write;
|
||||||
}
|
}
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: Device not ready recovery failed after %d attempts\n", max_recovery_attempts);
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Device not ready recovery failed after %d attempts\n", max_recovery_attempts);
|
||||||
|
}
|
||||||
return -2;
|
return -2;
|
||||||
} else {
|
} else {
|
||||||
// Other errors - limited retry for transient issues
|
// Other errors - limited retry for transient issues
|
||||||
recovery_attempts++;
|
recovery_attempts++;
|
||||||
if (recovery_attempts <= 1 && (pcm_rc == -EINTR || pcm_rc == -EBUSY)) {
|
if (recovery_attempts <= 1 && (pcm_rc == -EINTR || pcm_rc == -EBUSY)) {
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: Transient error %d (%s), retrying once\n", pcm_rc, snd_strerror(pcm_rc));
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Transient error %d (%s), retrying once\n", pcm_rc, snd_strerror(pcm_rc));
|
||||||
|
}
|
||||||
usleep(sleep_microseconds / 2);
|
usleep(sleep_microseconds / 2);
|
||||||
goto retry_write;
|
goto retry_write;
|
||||||
}
|
}
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: Unrecoverable error %d (%s)\n", pcm_rc, snd_strerror(pcm_rc));
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Unrecoverable error %d (%s)\n", pcm_rc, snd_strerror(pcm_rc));
|
||||||
|
}
|
||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("[AUDIO] jetkvm_audio_decode_write: Successfully wrote %d PCM frames to USB Gadget audio device\n", pcm_frames);
|
if (trace_logging_enabled) {
|
||||||
|
printf("[AUDIO_INPUT] jetkvm_audio_decode_write: Successfully wrote %d PCM frames to USB Gadget audio device\n", pcm_frames);
|
||||||
|
}
|
||||||
return pcm_frames;
|
return pcm_frames;
|
||||||
}
|
}
|
||||||
// Safe playback cleanup with double-close protection
|
|
||||||
|
// ============================================================================
|
||||||
|
// CLEANUP FUNCTIONS
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup audio INPUT path resources (client microphone → device speakers)
|
||||||
|
*
|
||||||
|
* Thread-safe cleanup with atomic operations to prevent double-cleanup
|
||||||
|
* Properly drains ALSA buffers before closing to avoid audio artifacts
|
||||||
|
*/
|
||||||
void jetkvm_audio_playback_close() {
|
void jetkvm_audio_playback_close() {
|
||||||
// Wait for any ongoing operations to complete
|
// Wait for any ongoing operations to complete
|
||||||
while (playback_initializing) {
|
while (playback_initializing) {
|
||||||
|
@ -597,7 +832,12 @@ void jetkvm_audio_playback_close() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safe capture cleanup
|
/**
|
||||||
|
* Cleanup audio OUTPUT path resources (device microphone → client speakers)
|
||||||
|
*
|
||||||
|
* Thread-safe cleanup with atomic operations to prevent double-cleanup
|
||||||
|
* Properly drains ALSA buffers before closing to avoid audio artifacts
|
||||||
|
*/
|
||||||
void jetkvm_audio_capture_close() {
|
void jetkvm_audio_capture_close() {
|
||||||
// Wait for any ongoing operations to complete
|
// Wait for any ongoing operations to complete
|
||||||
while (capture_initializing) {
|
while (capture_initializing) {
|
||||||
|
|
|
@ -5,10 +5,15 @@ package audio
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/jetkvm/kvm/internal/logging"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -19,9 +24,7 @@ import (
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
// Optimized Go wrappers with reduced overhead
|
|
||||||
var (
|
var (
|
||||||
// Base error types for wrapping with context
|
|
||||||
errAudioInitFailed = errors.New("failed to init ALSA/Opus")
|
errAudioInitFailed = errors.New("failed to init ALSA/Opus")
|
||||||
errAudioReadEncode = errors.New("audio read/encode error")
|
errAudioReadEncode = errors.New("audio read/encode error")
|
||||||
errAudioDecodeWrite = errors.New("audio decode/write error")
|
errAudioDecodeWrite = errors.New("audio decode/write error")
|
||||||
|
@ -91,6 +94,30 @@ func cgoAudioInit() error {
|
||||||
cache := GetCachedConfig()
|
cache := GetCachedConfig()
|
||||||
cache.Update()
|
cache.Update()
|
||||||
|
|
||||||
|
// Enable C trace logging if Go audio scope trace level is active
|
||||||
|
|
||||||
|
// Enable C trace logging if Go audio scope trace level is active
|
||||||
|
audioLogger := logging.GetSubsystemLogger("audio")
|
||||||
|
loggerTraceEnabled := audioLogger.GetLevel() <= zerolog.TraceLevel
|
||||||
|
|
||||||
|
// Manual check for audio scope in PION_LOG_TRACE (workaround for logging system bug)
|
||||||
|
manualTraceEnabled := false
|
||||||
|
pionTrace := os.Getenv("PION_LOG_TRACE")
|
||||||
|
if pionTrace != "" {
|
||||||
|
scopes := strings.Split(strings.ToLower(pionTrace), ",")
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if strings.TrimSpace(scope) == "audio" {
|
||||||
|
manualTraceEnabled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use manual check as fallback if logging system fails
|
||||||
|
traceEnabled := loggerTraceEnabled || manualTraceEnabled
|
||||||
|
|
||||||
|
CGOSetTraceLogging(traceEnabled)
|
||||||
|
|
||||||
// Update C constants from cached config (atomic access, no locks)
|
// Update C constants from cached config (atomic access, no locks)
|
||||||
C.update_audio_constants(
|
C.update_audio_constants(
|
||||||
C.int(cache.opusBitrate.Load()),
|
C.int(cache.opusBitrate.Load()),
|
||||||
|
@ -174,7 +201,7 @@ type AudioConfigCache struct {
|
||||||
|
|
||||||
// Global audio config cache instance
|
// Global audio config cache instance
|
||||||
var globalAudioConfigCache = &AudioConfigCache{
|
var globalAudioConfigCache = &AudioConfigCache{
|
||||||
cacheExpiry: 30 * time.Second, // Increased from 10s to 30s to further reduce cache updates
|
cacheExpiry: 30 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCachedConfig returns the global audio config cache instance
|
// GetCachedConfig returns the global audio config cache instance
|
||||||
|
@ -318,6 +345,10 @@ func cgoAudioPlaybackInit() error {
|
||||||
cache := GetCachedConfig()
|
cache := GetCachedConfig()
|
||||||
cache.Update()
|
cache.Update()
|
||||||
|
|
||||||
|
// Enable C trace logging if Go audio scope trace level is active
|
||||||
|
audioLogger := logging.GetSubsystemLogger("audio")
|
||||||
|
CGOSetTraceLogging(audioLogger.GetLevel() <= zerolog.TraceLevel)
|
||||||
|
|
||||||
// No need to update C constants here as they're already set in cgoAudioInit
|
// No need to update C constants here as they're already set in cgoAudioInit
|
||||||
|
|
||||||
ret := C.jetkvm_audio_playback_init()
|
ret := C.jetkvm_audio_playback_init()
|
||||||
|
@ -333,12 +364,12 @@ func cgoAudioPlaybackClose() {
|
||||||
|
|
||||||
// Audio decode/write metrics for monitoring USB Gadget audio success
|
// Audio decode/write metrics for monitoring USB Gadget audio success
|
||||||
var (
|
var (
|
||||||
audioDecodeWriteTotal atomic.Int64 // Total decode/write attempts
|
audioDecodeWriteTotal atomic.Int64
|
||||||
audioDecodeWriteSuccess atomic.Int64 // Successful decode/write operations
|
audioDecodeWriteSuccess atomic.Int64
|
||||||
audioDecodeWriteFailures atomic.Int64 // Failed decode/write operations
|
audioDecodeWriteFailures atomic.Int64
|
||||||
audioDecodeWriteRecovery atomic.Int64 // Recovery attempts
|
audioDecodeWriteRecovery atomic.Int64
|
||||||
audioDecodeWriteLastError atomic.Value // Last error (string)
|
audioDecodeWriteLastError atomic.Value
|
||||||
audioDecodeWriteLastTime atomic.Int64 // Last operation timestamp (unix nano)
|
audioDecodeWriteLastTime atomic.Int64
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetAudioDecodeWriteStats returns current audio decode/write statistics
|
// GetAudioDecodeWriteStats returns current audio decode/write statistics
|
||||||
|
@ -594,3 +625,13 @@ func CGOAudioDecodeWrite(opusData []byte, pcmBuffer []byte) (int, error) {
|
||||||
func CGOUpdateOpusEncoderParams(bitrate, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx int) error {
|
func CGOUpdateOpusEncoderParams(bitrate, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx int) error {
|
||||||
return updateOpusEncoderParams(bitrate, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx)
|
return updateOpusEncoderParams(bitrate, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CGOSetTraceLogging(enabled bool) {
|
||||||
|
var cEnabled C.int
|
||||||
|
if enabled {
|
||||||
|
cEnabled = 1
|
||||||
|
} else {
|
||||||
|
cEnabled = 0
|
||||||
|
}
|
||||||
|
C.set_trace_logging(cEnabled)
|
||||||
|
}
|
||||||
|
|
|
@ -241,11 +241,6 @@ func (s *AudioControlService) SetAudioQuality(quality AudioQuality) {
|
||||||
SetAudioQuality(quality)
|
SetAudioQuality(quality)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMicrophoneQuality sets the microphone input quality
|
|
||||||
func (s *AudioControlService) SetMicrophoneQuality(quality AudioQuality) {
|
|
||||||
SetMicrophoneQuality(quality)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAudioQualityPresets returns available audio quality presets
|
// GetAudioQualityPresets returns available audio quality presets
|
||||||
func (s *AudioControlService) GetAudioQualityPresets() map[AudioQuality]AudioConfig {
|
func (s *AudioControlService) GetAudioQualityPresets() map[AudioQuality]AudioConfig {
|
||||||
return GetAudioQualityPresets()
|
return GetAudioQualityPresets()
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
"github.com/jetkvm/kvm/internal/logging"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Global audio input server instance
|
// Global audio input server instance
|
||||||
|
@ -46,7 +47,7 @@ func RecoverGlobalAudioInputServer() {
|
||||||
// RunAudioInputServer runs the audio input server subprocess
|
// RunAudioInputServer runs the audio input server subprocess
|
||||||
// This should be called from main() when the subprocess is detected
|
// This should be called from main() when the subprocess is detected
|
||||||
func RunAudioInputServer() error {
|
func RunAudioInputServer() error {
|
||||||
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-server").Logger()
|
logger := logging.GetSubsystemLogger("audio").With().Str("component", "audio-input-server").Logger()
|
||||||
|
|
||||||
// Parse OPUS configuration from environment variables
|
// Parse OPUS configuration from environment variables
|
||||||
bitrate, complexity, vbr, signalType, bandwidth, dtx := parseOpusConfig()
|
bitrate, complexity, vbr, signalType, bandwidth, dtx := parseOpusConfig()
|
||||||
|
@ -85,6 +86,10 @@ func RunAudioInputServer() error {
|
||||||
|
|
||||||
logger.Info().Msg("audio input server started, waiting for connections")
|
logger.Info().Msg("audio input server started, waiting for connections")
|
||||||
|
|
||||||
|
// Update C trace logging based on current audio scope log level (after environment variables are processed)
|
||||||
|
traceEnabled := logger.GetLevel() <= zerolog.TraceLevel
|
||||||
|
CGOSetTraceLogging(traceEnabled)
|
||||||
|
|
||||||
// Set up signal handling for graceful shutdown
|
// Set up signal handling for graceful shutdown
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
|
@ -119,6 +119,11 @@ func (ais *AudioInputSupervisor) startProcess() error {
|
||||||
// Set environment variables for IPC and OPUS configuration
|
// Set environment variables for IPC and OPUS configuration
|
||||||
env := append(os.Environ(), "JETKVM_AUDIO_INPUT_IPC=true") // Enable IPC mode
|
env := append(os.Environ(), "JETKVM_AUDIO_INPUT_IPC=true") // Enable IPC mode
|
||||||
env = append(env, ais.opusEnv...) // Add OPUS configuration
|
env = append(env, ais.opusEnv...) // Add OPUS configuration
|
||||||
|
|
||||||
|
// Pass logging environment variables directly to subprocess
|
||||||
|
// The subprocess will inherit all PION_LOG_* variables from os.Environ()
|
||||||
|
// This ensures the audio scope gets the correct trace level
|
||||||
|
|
||||||
ais.cmd.Env = env
|
ais.cmd.Env = env
|
||||||
|
|
||||||
// Set process group to allow clean termination
|
// Set process group to allow clean termination
|
||||||
|
|
|
@ -4,10 +4,12 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
"github.com/jetkvm/kvm/internal/logging"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// getEnvInt reads an integer from environment variable with a default value
|
// getEnvInt reads an integer from environment variable with a default value
|
||||||
|
@ -15,7 +17,7 @@ import (
|
||||||
// RunAudioOutputServer runs the audio output server subprocess
|
// RunAudioOutputServer runs the audio output server subprocess
|
||||||
// This should be called from main() when the subprocess is detected
|
// This should be called from main() when the subprocess is detected
|
||||||
func RunAudioOutputServer() error {
|
func RunAudioOutputServer() error {
|
||||||
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-server").Logger()
|
logger := logging.GetSubsystemLogger("audio").With().Str("component", "audio-output-server").Logger()
|
||||||
|
|
||||||
// Parse OPUS configuration from environment variables
|
// Parse OPUS configuration from environment variables
|
||||||
bitrate, complexity, vbr, signalType, bandwidth, dtx := parseOpusConfig()
|
bitrate, complexity, vbr, signalType, bandwidth, dtx := parseOpusConfig()
|
||||||
|
@ -52,6 +54,27 @@ func RunAudioOutputServer() error {
|
||||||
|
|
||||||
logger.Info().Msg("audio output server started, waiting for connections")
|
logger.Info().Msg("audio output server started, waiting for connections")
|
||||||
|
|
||||||
|
// Update C trace logging based on current audio scope log level (after environment variables are processed)
|
||||||
|
loggerTraceEnabled := logger.GetLevel() <= zerolog.TraceLevel
|
||||||
|
|
||||||
|
// Manual check for audio scope in PION_LOG_TRACE (workaround for logging system bug)
|
||||||
|
manualTraceEnabled := false
|
||||||
|
pionTrace := os.Getenv("PION_LOG_TRACE")
|
||||||
|
if pionTrace != "" {
|
||||||
|
scopes := strings.Split(strings.ToLower(pionTrace), ",")
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if strings.TrimSpace(scope) == "audio" {
|
||||||
|
manualTraceEnabled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use manual check as fallback if logging system fails
|
||||||
|
traceEnabled := loggerTraceEnabled || manualTraceEnabled
|
||||||
|
|
||||||
|
CGOSetTraceLogging(traceEnabled)
|
||||||
|
|
||||||
// Set up signal handling for graceful shutdown
|
// Set up signal handling for graceful shutdown
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
|
@ -217,7 +217,13 @@ func (s *AudioOutputSupervisor) startProcess() error {
|
||||||
s.cmd.Stderr = os.Stderr
|
s.cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
// Set environment variables for OPUS configuration
|
// Set environment variables for OPUS configuration
|
||||||
s.cmd.Env = append(os.Environ(), s.opusEnv...)
|
env := append(os.Environ(), s.opusEnv...)
|
||||||
|
|
||||||
|
// Pass logging environment variables directly to subprocess
|
||||||
|
// The subprocess will inherit all PION_LOG_* variables from os.Environ()
|
||||||
|
// This ensures the audio scope gets the correct trace level
|
||||||
|
|
||||||
|
s.cmd.Env = env
|
||||||
|
|
||||||
// Start the process
|
// Start the process
|
||||||
if err := s.cmd.Start(); err != nil {
|
if err := s.cmd.Start(); err != nil {
|
||||||
|
|
|
@ -260,97 +260,6 @@ func GetAudioConfig() AudioConfig {
|
||||||
return currentConfig
|
return currentConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simplified OPUS parameter lookup table
|
|
||||||
var opusParams = map[AudioQuality]struct {
|
|
||||||
complexity, vbr, signalType, bandwidth, dtx int
|
|
||||||
}{
|
|
||||||
AudioQualityLow: {Config.AudioQualityLowOpusComplexity, Config.AudioQualityLowOpusVBR, Config.AudioQualityLowOpusSignalType, Config.AudioQualityLowOpusBandwidth, Config.AudioQualityLowOpusDTX},
|
|
||||||
AudioQualityMedium: {Config.AudioQualityMediumOpusComplexity, Config.AudioQualityMediumOpusVBR, Config.AudioQualityMediumOpusSignalType, Config.AudioQualityMediumOpusBandwidth, Config.AudioQualityMediumOpusDTX},
|
|
||||||
AudioQualityHigh: {Config.AudioQualityHighOpusComplexity, Config.AudioQualityHighOpusVBR, Config.AudioQualityHighOpusSignalType, Config.AudioQualityHighOpusBandwidth, Config.AudioQualityHighOpusDTX},
|
|
||||||
AudioQualityUltra: {Config.AudioQualityUltraOpusComplexity, Config.AudioQualityUltraOpusVBR, Config.AudioQualityUltraOpusSignalType, Config.AudioQualityUltraOpusBandwidth, Config.AudioQualityUltraOpusDTX},
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMicrophoneQuality updates the current microphone quality configuration
|
|
||||||
func SetMicrophoneQuality(quality AudioQuality) {
|
|
||||||
// Validate audio quality parameter
|
|
||||||
if err := ValidateAudioQuality(quality); err != nil {
|
|
||||||
// Log validation error but don't fail - maintain backward compatibility
|
|
||||||
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
|
|
||||||
logger.Warn().Err(err).Int("quality", int(quality)).Msg("invalid microphone quality, using current config")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
presets := GetMicrophoneQualityPresets()
|
|
||||||
if config, exists := presets[quality]; exists {
|
|
||||||
currentMicrophoneConfig = config
|
|
||||||
|
|
||||||
// Get OPUS parameters using lookup table
|
|
||||||
params, exists := opusParams[quality]
|
|
||||||
if !exists {
|
|
||||||
// Fallback to medium quality
|
|
||||||
params = opusParams[AudioQualityMedium]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update audio input subprocess configuration dynamically without restart
|
|
||||||
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
|
|
||||||
|
|
||||||
// Set new OPUS configuration for future restarts
|
|
||||||
if supervisor := GetAudioInputSupervisor(); supervisor != nil {
|
|
||||||
supervisor.SetOpusConfig(config.Bitrate*1000, params.complexity, params.vbr, params.signalType, params.bandwidth, params.dtx)
|
|
||||||
|
|
||||||
// Check if microphone is active but IPC control is broken
|
|
||||||
inputManager := getAudioInputManager()
|
|
||||||
if inputManager.IsRunning() && !supervisor.IsConnected() {
|
|
||||||
// Reconnect the IPC control channel
|
|
||||||
supervisor.Stop()
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
if err := supervisor.Start(); err != nil {
|
|
||||||
logger.Debug().Err(err).Msg("failed to reconnect IPC control channel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send dynamic configuration update to running subprocess via IPC
|
|
||||||
if supervisor.IsConnected() {
|
|
||||||
// Convert AudioConfig to UnifiedIPCOpusConfig with complete Opus parameters
|
|
||||||
opusConfig := UnifiedIPCOpusConfig{
|
|
||||||
SampleRate: config.SampleRate,
|
|
||||||
Channels: config.Channels,
|
|
||||||
FrameSize: int(config.FrameSize.Milliseconds() * int64(config.SampleRate) / 1000), // Convert ms to samples
|
|
||||||
Bitrate: config.Bitrate * 1000, // Convert kbps to bps
|
|
||||||
Complexity: params.complexity,
|
|
||||||
VBR: params.vbr,
|
|
||||||
SignalType: params.signalType,
|
|
||||||
Bandwidth: params.bandwidth,
|
|
||||||
DTX: params.dtx,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := supervisor.SendOpusConfig(opusConfig); err != nil {
|
|
||||||
logger.Debug().Err(err).Msg("failed to send dynamic Opus config update via IPC")
|
|
||||||
// Fallback to subprocess restart if IPC update fails
|
|
||||||
supervisor.Stop()
|
|
||||||
if err := supervisor.Start(); err != nil {
|
|
||||||
logger.Error().Err(err).Msg("failed to restart audio input subprocess after IPC update failure")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.Info().Msg("audio input quality updated dynamically via IPC")
|
|
||||||
|
|
||||||
// Reset audio input stats after config update
|
|
||||||
go func() {
|
|
||||||
time.Sleep(Config.QualityChangeSettleDelay) // Wait for quality change to settle
|
|
||||||
// Reset audio input server stats to clear persistent warnings
|
|
||||||
ResetGlobalAudioInputServerStats()
|
|
||||||
// Attempt recovery if microphone is still having issues
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
RecoverGlobalAudioInputServer()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.Info().Bool("supervisor_running", supervisor.IsRunning()).Msg("audio input subprocess not connected, configuration will apply on next start")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMicrophoneConfig returns the current microphone configuration
|
// GetMicrophoneConfig returns the current microphone configuration
|
||||||
func GetMicrophoneConfig() AudioConfig {
|
func GetMicrophoneConfig() AudioConfig {
|
||||||
return currentMicrophoneConfig
|
return currentMicrophoneConfig
|
||||||
|
|
Loading…
Reference in New Issue