mirror of https://github.com/jetkvm/kvm.git
789 lines
26 KiB
C
789 lines
26 KiB
C
/*
|
|
* JetKVM Audio Processing Module
|
|
*
|
|
* Bidirectional audio processing optimized for ARM NEON SIMD:
|
|
* - OUTPUT PATH: TC358743 HDMI audio → Client speakers
|
|
* Pipeline: ALSA hw:0,0 capture → Opus encode (128kbps, FEC enabled)
|
|
*
|
|
* - INPUT PATH: Client microphone → Device speakers
|
|
* Pipeline: Opus decode (with FEC) → ALSA hw:1,0 playback
|
|
*
|
|
* Key features:
|
|
* - ARM NEON SIMD optimization for all audio operations
|
|
* - Opus in-band FEC for packet loss resilience
|
|
* - S16_LE @ 48kHz stereo, 20ms frames (960 samples)
|
|
*/
|
|
|
|
#include <alsa/asoundlib.h>
|
|
#include <opus.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <sched.h>
|
|
#include <time.h>
|
|
#include <signal.h>
|
|
|
|
// ARM NEON SIMD support (always available on JetKVM's ARM Cortex-A7)
|
|
#include <arm_neon.h>
|
|
|
|
// RV1106 (Cortex-A7) has 64-byte cache lines
|
|
#define CACHE_LINE_SIZE 64
|
|
#define SIMD_ALIGN __attribute__((aligned(16)))
|
|
#define CACHE_ALIGN __attribute__((aligned(CACHE_LINE_SIZE)))
|
|
#define SIMD_PREFETCH(addr, rw, locality) __builtin_prefetch(addr, rw, locality)
|
|
|
|
// Compile-time trace logging - disabled for production (zero overhead)
|
|
#define TRACE_LOG(...) ((void)0)
|
|
|
|
// ALSA device handles
|
|
static snd_pcm_t *pcm_capture_handle = NULL; // OUTPUT: TC358743 HDMI audio → client
|
|
static snd_pcm_t *pcm_playback_handle = NULL; // INPUT: Client microphone → device speakers
|
|
|
|
// ALSA device names
|
|
static const char *alsa_capture_device = NULL;
|
|
static const char *alsa_playback_device = NULL;
|
|
|
|
// Opus codec instances
|
|
static OpusEncoder *encoder = NULL;
|
|
static OpusDecoder *decoder = NULL;
|
|
|
|
// Audio format (S16_LE @ 48kHz stereo)
|
|
static uint32_t sample_rate = 48000;
|
|
static uint8_t channels = 2;
|
|
static uint16_t frame_size = 960; // 20ms frames at 48kHz
|
|
|
|
static uint32_t opus_bitrate = 128000;
|
|
static uint8_t opus_complexity = 2;
|
|
static uint16_t max_packet_size = 1500;
|
|
|
|
// Opus encoder constants (hardcoded for production)
|
|
#define OPUS_VBR 1 // VBR enabled
|
|
#define OPUS_VBR_CONSTRAINT 0 // Unconstrained VBR (better for low-volume signals)
|
|
#define OPUS_SIGNAL_TYPE 3002 // OPUS_SIGNAL_MUSIC (better transient handling)
|
|
#define OPUS_BANDWIDTH 1105 // OPUS_BANDWIDTH_FULLBAND (20kHz, enabled by 128kbps bitrate)
|
|
#define OPUS_DTX 0 // DTX disabled (prevents audio drops)
|
|
#define OPUS_LSB_DEPTH 16 // 16-bit depth
|
|
|
|
// ALSA retry configuration
|
|
static uint32_t sleep_microseconds = 1000;
|
|
static uint32_t sleep_milliseconds = 1; // Precomputed: sleep_microseconds / 1000
|
|
static uint8_t max_attempts_global = 5;
|
|
static uint32_t max_backoff_us_global = 500000;
|
|
|
|
int jetkvm_audio_capture_init();
|
|
void jetkvm_audio_capture_close();
|
|
int jetkvm_audio_read_encode(void *opus_buf);
|
|
|
|
int jetkvm_audio_playback_init();
|
|
void jetkvm_audio_playback_close();
|
|
int jetkvm_audio_decode_write(void *opus_buf, int opus_size);
|
|
|
|
void update_audio_constants(uint32_t bitrate, uint8_t complexity,
|
|
uint32_t sr, uint8_t ch, uint16_t fs, uint16_t max_pkt,
|
|
uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff);
|
|
void update_audio_decoder_constants(uint32_t sr, uint8_t ch, uint16_t fs, uint16_t max_pkt,
|
|
uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff);
|
|
int update_opus_encoder_params(uint32_t bitrate, uint8_t complexity);
|
|
|
|
|
|
/**
|
|
* Sync encoder configuration from Go to C
|
|
*/
|
|
void update_audio_constants(uint32_t bitrate, uint8_t complexity,
|
|
uint32_t sr, uint8_t ch, uint16_t fs, uint16_t max_pkt,
|
|
uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff) {
|
|
opus_bitrate = bitrate;
|
|
opus_complexity = complexity;
|
|
sample_rate = sr;
|
|
channels = ch;
|
|
frame_size = fs;
|
|
max_packet_size = max_pkt;
|
|
sleep_microseconds = sleep_us;
|
|
sleep_milliseconds = sleep_us / 1000; // Precompute for snd_pcm_wait
|
|
max_attempts_global = max_attempts;
|
|
max_backoff_us_global = max_backoff;
|
|
}
|
|
|
|
/**
|
|
* Sync decoder configuration from Go to C (no encoder-only params)
|
|
*/
|
|
void update_audio_decoder_constants(uint32_t sr, uint8_t ch, uint16_t fs, uint16_t max_pkt,
|
|
uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff) {
|
|
sample_rate = sr;
|
|
channels = ch;
|
|
frame_size = fs;
|
|
max_packet_size = max_pkt;
|
|
sleep_microseconds = sleep_us;
|
|
sleep_milliseconds = sleep_us / 1000; // Precompute for snd_pcm_wait
|
|
max_attempts_global = max_attempts;
|
|
max_backoff_us_global = max_backoff;
|
|
}
|
|
|
|
/**
|
|
* Initialize ALSA device names from environment variables
|
|
* Must be called before jetkvm_audio_capture_init or jetkvm_audio_playback_init
|
|
*/
|
|
static void init_alsa_devices_from_env(void) {
|
|
if (alsa_capture_device == NULL) {
|
|
alsa_capture_device = getenv("ALSA_CAPTURE_DEVICE");
|
|
if (alsa_capture_device == NULL || alsa_capture_device[0] == '\0') {
|
|
alsa_capture_device = "hw:0,0"; // Default to HDMI
|
|
}
|
|
}
|
|
if (alsa_playback_device == NULL) {
|
|
alsa_playback_device = getenv("ALSA_PLAYBACK_DEVICE");
|
|
if (alsa_playback_device == NULL || alsa_playback_device[0] == '\0') {
|
|
alsa_playback_device = "hw:1,0"; // Default to USB gadget
|
|
}
|
|
}
|
|
}
|
|
|
|
// SIMD-OPTIMIZED BUFFER OPERATIONS (ARM NEON)
|
|
|
|
/**
|
|
* Clear audio buffer using NEON (16 samples/iteration with 2x unrolling)
|
|
*/
|
|
static inline void simd_clear_samples_s16(short * __restrict__ buffer, uint32_t samples) {
|
|
const int16x8_t zero = vdupq_n_s16(0);
|
|
uint32_t i = 0;
|
|
|
|
// Process 16 samples at a time (2x unrolled for better pipeline utilization)
|
|
uint32_t simd_samples = samples & ~15U;
|
|
for (; i < simd_samples; i += 16) {
|
|
vst1q_s16(&buffer[i], zero);
|
|
vst1q_s16(&buffer[i + 8], zero);
|
|
}
|
|
|
|
// Handle remaining 8 samples
|
|
if (i + 8 <= samples) {
|
|
vst1q_s16(&buffer[i], zero);
|
|
i += 8;
|
|
}
|
|
|
|
// Scalar: remaining samples
|
|
for (; i < samples; i++) {
|
|
buffer[i] = 0;
|
|
}
|
|
}
|
|
|
|
// INITIALIZATION STATE TRACKING
|
|
|
|
static volatile sig_atomic_t capture_initializing = 0;
|
|
static volatile sig_atomic_t capture_initialized = 0;
|
|
static volatile sig_atomic_t playback_initializing = 0;
|
|
static volatile sig_atomic_t playback_initialized = 0;
|
|
|
|
/**
|
|
* Update Opus encoder settings at runtime (does NOT modify FEC or hardcoded settings)
|
|
* @return 0 on success, -1 if not initialized, >0 if some settings failed
|
|
*/
|
|
int update_opus_encoder_params(uint32_t bitrate, uint8_t complexity) {
|
|
if (!encoder || !capture_initialized) {
|
|
return -1;
|
|
}
|
|
|
|
// Update runtime-configurable parameters
|
|
opus_bitrate = bitrate;
|
|
opus_complexity = complexity;
|
|
|
|
// Apply settings to encoder
|
|
int result = 0;
|
|
result |= opus_encoder_ctl(encoder, OPUS_SET_BITRATE(opus_bitrate));
|
|
result |= opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(opus_complexity));
|
|
|
|
return result;
|
|
}
|
|
|
|
// ALSA UTILITY FUNCTIONS
|
|
|
|
/**
|
|
* Open ALSA device with exponential backoff retry
|
|
* @return 0 on success, negative error code on failure
|
|
*/
|
|
// Helper: High-precision sleep using nanosleep (better than usleep)
|
|
static inline void precise_sleep_us(uint32_t microseconds) {
|
|
struct timespec ts = {
|
|
.tv_sec = microseconds / 1000000,
|
|
.tv_nsec = (microseconds % 1000000) * 1000
|
|
};
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
|
|
static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream_t stream) {
|
|
uint8_t attempt = 0;
|
|
int err;
|
|
uint32_t backoff_us = sleep_microseconds;
|
|
|
|
while (attempt < max_attempts_global) {
|
|
err = snd_pcm_open(handle, device, stream, SND_PCM_NONBLOCK);
|
|
if (err >= 0) {
|
|
snd_pcm_nonblock(*handle, 0);
|
|
return 0;
|
|
}
|
|
|
|
attempt++;
|
|
|
|
// Exponential backoff with bit shift (faster than multiplication)
|
|
if (err == -EBUSY || err == -EAGAIN) {
|
|
precise_sleep_us(backoff_us);
|
|
backoff_us = (backoff_us << 1 < max_backoff_us_global) ? (backoff_us << 1) : max_backoff_us_global;
|
|
} else if (err == -ENODEV || err == -ENOENT) {
|
|
precise_sleep_us(backoff_us << 1);
|
|
backoff_us = (backoff_us << 1 < max_backoff_us_global) ? (backoff_us << 1) : max_backoff_us_global;
|
|
} else if (err == -EPERM || err == -EACCES) {
|
|
precise_sleep_us(backoff_us >> 1);
|
|
} else {
|
|
precise_sleep_us(backoff_us);
|
|
backoff_us = (backoff_us << 1 < max_backoff_us_global) ? (backoff_us << 1) : max_backoff_us_global;
|
|
}
|
|
}
|
|
return err;
|
|
}
|
|
|
|
/**
|
|
* Configure ALSA device (S16_LE @ 48kHz stereo with optimized buffering)
|
|
* @param handle ALSA PCM handle
|
|
* @param device_name Unused (for debugging only)
|
|
* @return 0 on success, negative error code on failure
|
|
*/
|
|
static int configure_alsa_device(snd_pcm_t *handle, const char *device_name) {
|
|
snd_pcm_hw_params_t *params;
|
|
snd_pcm_sw_params_t *sw_params;
|
|
int err;
|
|
|
|
if (!handle) return -1;
|
|
|
|
snd_pcm_hw_params_alloca(¶ms);
|
|
snd_pcm_sw_params_alloca(&sw_params);
|
|
|
|
err = snd_pcm_hw_params_any(handle, params);
|
|
if (err < 0) return err;
|
|
|
|
err = snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);
|
|
if (err < 0) return err;
|
|
|
|
err = snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE);
|
|
if (err < 0) return err;
|
|
|
|
err = snd_pcm_hw_params_set_channels(handle, params, channels);
|
|
if (err < 0) return err;
|
|
|
|
err = snd_pcm_hw_params_set_rate(handle, params, sample_rate, 0);
|
|
if (err < 0) {
|
|
unsigned int rate = sample_rate;
|
|
err = snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0);
|
|
if (err < 0) return err;
|
|
}
|
|
|
|
snd_pcm_uframes_t period_size = frame_size; // Optimized: use full frame as period
|
|
if (period_size < 64) period_size = 64;
|
|
|
|
err = snd_pcm_hw_params_set_period_size_near(handle, params, &period_size, 0);
|
|
if (err < 0) return err;
|
|
|
|
snd_pcm_uframes_t buffer_size = period_size * 2; // Optimized: minimal buffer for low latency
|
|
err = snd_pcm_hw_params_set_buffer_size_near(handle, params, &buffer_size);
|
|
if (err < 0) return err;
|
|
|
|
err = snd_pcm_hw_params(handle, params);
|
|
if (err < 0) return err;
|
|
|
|
err = snd_pcm_sw_params_current(handle, sw_params);
|
|
if (err < 0) return err;
|
|
|
|
err = snd_pcm_sw_params_set_start_threshold(handle, sw_params, period_size);
|
|
if (err < 0) return err;
|
|
|
|
err = snd_pcm_sw_params_set_avail_min(handle, sw_params, period_size);
|
|
if (err < 0) return err;
|
|
|
|
err = snd_pcm_sw_params(handle, sw_params);
|
|
if (err < 0) return err;
|
|
|
|
return snd_pcm_prepare(handle);
|
|
}
|
|
|
|
// AUDIO OUTPUT PATH FUNCTIONS (TC358743 HDMI Audio → Client Speakers)
|
|
|
|
/**
|
|
* Initialize OUTPUT path (TC358743 HDMI capture → Opus encoder)
|
|
* Opens hw:0,0 (TC358743) and creates Opus encoder with optimized settings
|
|
* @return 0 on success, -EBUSY if initializing, -1/-2/-3 on errors
|
|
*/
|
|
int jetkvm_audio_capture_init() {
|
|
int err;
|
|
|
|
init_alsa_devices_from_env();
|
|
|
|
if (__sync_bool_compare_and_swap(&capture_initializing, 0, 1) == 0) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
if (capture_initialized) {
|
|
capture_initializing = 0;
|
|
return 0;
|
|
}
|
|
|
|
if (encoder) {
|
|
opus_encoder_destroy(encoder);
|
|
encoder = NULL;
|
|
}
|
|
if (pcm_capture_handle) {
|
|
snd_pcm_close(pcm_capture_handle);
|
|
pcm_capture_handle = NULL;
|
|
}
|
|
|
|
err = safe_alsa_open(&pcm_capture_handle, alsa_capture_device, SND_PCM_STREAM_CAPTURE);
|
|
if (err < 0) {
|
|
fprintf(stderr, "Failed to open ALSA capture device %s: %s\n",
|
|
alsa_capture_device, snd_strerror(err));
|
|
fflush(stderr);
|
|
capture_initializing = 0;
|
|
return -1;
|
|
}
|
|
|
|
err = configure_alsa_device(pcm_capture_handle, "capture");
|
|
if (err < 0) {
|
|
snd_pcm_close(pcm_capture_handle);
|
|
pcm_capture_handle = NULL;
|
|
capture_initializing = 0;
|
|
return -2;
|
|
}
|
|
|
|
int opus_err = 0;
|
|
encoder = opus_encoder_create(sample_rate, channels, OPUS_APPLICATION_AUDIO, &opus_err);
|
|
if (!encoder || opus_err != OPUS_OK) {
|
|
if (pcm_capture_handle) {
|
|
snd_pcm_close(pcm_capture_handle);
|
|
pcm_capture_handle = NULL;
|
|
}
|
|
capture_initializing = 0;
|
|
return -3;
|
|
}
|
|
|
|
// Configure encoder with optimized settings
|
|
opus_encoder_ctl(encoder, OPUS_SET_BITRATE(opus_bitrate));
|
|
opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(opus_complexity));
|
|
opus_encoder_ctl(encoder, OPUS_SET_VBR(OPUS_VBR));
|
|
opus_encoder_ctl(encoder, OPUS_SET_VBR_CONSTRAINT(OPUS_VBR_CONSTRAINT));
|
|
opus_encoder_ctl(encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_TYPE));
|
|
opus_encoder_ctl(encoder, OPUS_SET_BANDWIDTH(OPUS_BANDWIDTH));
|
|
opus_encoder_ctl(encoder, OPUS_SET_DTX(OPUS_DTX));
|
|
opus_encoder_ctl(encoder, OPUS_SET_LSB_DEPTH(OPUS_LSB_DEPTH));
|
|
|
|
opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(1));
|
|
opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(20));
|
|
|
|
capture_initialized = 1;
|
|
capture_initializing = 0;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Read HDMI audio, encode to Opus (OUTPUT path hot function)
|
|
* @param opus_buf Output buffer for encoded Opus packet
|
|
* @return >0 = Opus packet size in bytes, -1 = error
|
|
*/
|
|
__attribute__((hot)) int jetkvm_audio_read_encode(void * __restrict__ opus_buf) {
|
|
static short CACHE_ALIGN pcm_buffer[960 * 2]; // Cache-aligned
|
|
unsigned char * __restrict__ out = (unsigned char*)opus_buf;
|
|
int32_t pcm_rc, nb_bytes;
|
|
int32_t err = 0;
|
|
uint8_t recovery_attempts = 0;
|
|
const uint8_t max_recovery_attempts = 3;
|
|
|
|
// Prefetch for write (out) and read (pcm_buffer) - RV1106 has small L1 cache
|
|
SIMD_PREFETCH(out, 1, 0); // Write, immediate use
|
|
SIMD_PREFETCH(pcm_buffer, 0, 0); // Read, immediate use
|
|
SIMD_PREFETCH(pcm_buffer + 64, 0, 1); // Prefetch next cache line
|
|
|
|
if (__builtin_expect(!capture_initialized || !pcm_capture_handle || !encoder || !opus_buf, 0)) {
|
|
TRACE_LOG("[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;
|
|
}
|
|
|
|
retry_read:
|
|
// Read 960 frames (20ms) from ALSA capture device
|
|
pcm_rc = snd_pcm_readi(pcm_capture_handle, pcm_buffer, frame_size);
|
|
|
|
if (__builtin_expect(pcm_rc < 0, 0)) {
|
|
if (pcm_rc == -EPIPE) {
|
|
recovery_attempts++;
|
|
if (recovery_attempts > max_recovery_attempts) {
|
|
return -1;
|
|
}
|
|
err = snd_pcm_prepare(pcm_capture_handle);
|
|
if (err < 0) {
|
|
snd_pcm_drop(pcm_capture_handle);
|
|
err = snd_pcm_prepare(pcm_capture_handle);
|
|
if (err < 0) return -1;
|
|
}
|
|
goto retry_read;
|
|
} else if (pcm_rc == -EAGAIN) {
|
|
// Wait for data to be available
|
|
snd_pcm_wait(pcm_capture_handle, sleep_milliseconds);
|
|
goto retry_read;
|
|
} else if (pcm_rc == -ESTRPIPE) {
|
|
recovery_attempts++;
|
|
if (recovery_attempts > max_recovery_attempts) {
|
|
return -1;
|
|
}
|
|
uint8_t resume_attempts = 0;
|
|
while ((err = snd_pcm_resume(pcm_capture_handle)) == -EAGAIN && resume_attempts < 10) {
|
|
snd_pcm_wait(pcm_capture_handle, sleep_milliseconds);
|
|
resume_attempts++;
|
|
}
|
|
if (err < 0) {
|
|
err = snd_pcm_prepare(pcm_capture_handle);
|
|
if (err < 0) return -1;
|
|
}
|
|
return 0;
|
|
} else if (pcm_rc == -ENODEV) {
|
|
return -1;
|
|
} else if (pcm_rc == -EIO) {
|
|
recovery_attempts++;
|
|
if (recovery_attempts <= max_recovery_attempts) {
|
|
snd_pcm_drop(pcm_capture_handle);
|
|
err = snd_pcm_prepare(pcm_capture_handle);
|
|
if (err >= 0) {
|
|
goto retry_read;
|
|
}
|
|
}
|
|
return -1;
|
|
} else {
|
|
recovery_attempts++;
|
|
if (recovery_attempts <= 1 && pcm_rc == -EINTR) {
|
|
goto retry_read;
|
|
} else if (recovery_attempts <= 1 && pcm_rc == -EBUSY) {
|
|
snd_pcm_wait(pcm_capture_handle, 1); // Wait 1ms for device
|
|
goto retry_read;
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// Zero-pad if we got a short read
|
|
if (__builtin_expect(pcm_rc < frame_size, 0)) {
|
|
uint32_t remaining_samples = (frame_size - pcm_rc) * channels;
|
|
simd_clear_samples_s16(&pcm_buffer[pcm_rc * channels], remaining_samples);
|
|
}
|
|
|
|
// Find peak amplitude with NEON SIMD
|
|
uint32_t total_samples = frame_size * channels;
|
|
int16x8_t vmax = vdupq_n_s16(0);
|
|
|
|
uint32_t i;
|
|
for (i = 0; i + 8 <= total_samples; i += 8) {
|
|
int16x8_t v = vld1q_s16(&pcm_buffer[i]);
|
|
int16x8_t vabs = vabsq_s16(v);
|
|
vmax = vmaxq_s16(vmax, vabs);
|
|
}
|
|
|
|
// Horizontal max reduction (manual for ARMv7)
|
|
int16x4_t vmax_low = vget_low_s16(vmax);
|
|
int16x4_t vmax_high = vget_high_s16(vmax);
|
|
int16x4_t vmax_reduced = vmax_s16(vmax_low, vmax_high);
|
|
vmax_reduced = vpmax_s16(vmax_reduced, vmax_reduced);
|
|
vmax_reduced = vpmax_s16(vmax_reduced, vmax_reduced);
|
|
int16_t peak = vget_lane_s16(vmax_reduced, 0);
|
|
|
|
// Handle remaining samples
|
|
for (; i < total_samples; i++) {
|
|
int16_t abs_val = (pcm_buffer[i] < 0) ? -pcm_buffer[i] : pcm_buffer[i];
|
|
if (abs_val > peak) peak = abs_val;
|
|
}
|
|
|
|
// Apply gain if signal is weak (below -18dB = 4096) for best quality
|
|
// Target: boost to ~50% of range (16384) to improve SNR
|
|
if (peak > 0 && peak < 4096) {
|
|
float gain = 16384.0f / peak;
|
|
if (gain > 8.0f) gain = 8.0f; // Max 18dB boost for best quality
|
|
|
|
// Apply gain with NEON and saturation
|
|
float32x4_t vgain = vdupq_n_f32(gain);
|
|
for (i = 0; i + 8 <= total_samples; i += 8) {
|
|
int16x8_t v = vld1q_s16(&pcm_buffer[i]);
|
|
|
|
// Convert to float, apply gain, saturate back to int16
|
|
int32x4_t v_low = vmovl_s16(vget_low_s16(v));
|
|
int32x4_t v_high = vmovl_s16(vget_high_s16(v));
|
|
|
|
float32x4_t f_low = vcvtq_f32_s32(v_low);
|
|
float32x4_t f_high = vcvtq_f32_s32(v_high);
|
|
|
|
f_low = vmulq_f32(f_low, vgain);
|
|
f_high = vmulq_f32(f_high, vgain);
|
|
|
|
v_low = vcvtq_s32_f32(f_low);
|
|
v_high = vcvtq_s32_f32(f_high);
|
|
|
|
// Saturate to int16 range
|
|
int16x4_t result_low = vqmovn_s32(v_low);
|
|
int16x4_t result_high = vqmovn_s32(v_high);
|
|
|
|
vst1q_s16(&pcm_buffer[i], vcombine_s16(result_low, result_high));
|
|
}
|
|
|
|
// Handle remaining samples
|
|
for (; i < total_samples; i++) {
|
|
int32_t boosted = (int32_t)(pcm_buffer[i] * gain);
|
|
if (boosted > 32767) boosted = 32767;
|
|
if (boosted < -32768) boosted = -32768;
|
|
pcm_buffer[i] = (int16_t)boosted;
|
|
}
|
|
}
|
|
|
|
nb_bytes = opus_encode(encoder, pcm_buffer, frame_size, out, max_packet_size);
|
|
return nb_bytes;
|
|
}
|
|
|
|
// AUDIO INPUT PATH FUNCTIONS (Client Microphone → Device Speakers)
|
|
|
|
/**
|
|
* Initialize INPUT path (Opus decoder → device speakers)
|
|
* Opens hw:1,0 (USB gadget) or "default" and creates Opus decoder
|
|
* @return 0 on success, -EBUSY if initializing, -1/-2 on errors
|
|
*/
|
|
int jetkvm_audio_playback_init() {
|
|
int err;
|
|
|
|
init_alsa_devices_from_env();
|
|
|
|
if (__sync_bool_compare_and_swap(&playback_initializing, 0, 1) == 0) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
if (playback_initialized) {
|
|
playback_initializing = 0;
|
|
return 0;
|
|
}
|
|
|
|
if (decoder) {
|
|
opus_decoder_destroy(decoder);
|
|
decoder = NULL;
|
|
}
|
|
if (pcm_playback_handle) {
|
|
snd_pcm_close(pcm_playback_handle);
|
|
pcm_playback_handle = NULL;
|
|
}
|
|
|
|
err = safe_alsa_open(&pcm_playback_handle, alsa_playback_device, SND_PCM_STREAM_PLAYBACK);
|
|
if (err < 0) {
|
|
fprintf(stderr, "Failed to open ALSA playback device %s: %s\n",
|
|
alsa_playback_device, snd_strerror(err));
|
|
fflush(stderr);
|
|
err = safe_alsa_open(&pcm_playback_handle, "default", SND_PCM_STREAM_PLAYBACK);
|
|
if (err < 0) {
|
|
playback_initializing = 0;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
err = configure_alsa_device(pcm_playback_handle, "playback");
|
|
if (err < 0) {
|
|
snd_pcm_close(pcm_playback_handle);
|
|
pcm_playback_handle = NULL;
|
|
playback_initializing = 0;
|
|
return -1;
|
|
}
|
|
|
|
int opus_err = 0;
|
|
decoder = opus_decoder_create(sample_rate, channels, &opus_err);
|
|
if (!decoder || opus_err != OPUS_OK) {
|
|
snd_pcm_close(pcm_playback_handle);
|
|
pcm_playback_handle = NULL;
|
|
playback_initializing = 0;
|
|
return -2;
|
|
}
|
|
|
|
playback_initialized = 1;
|
|
playback_initializing = 0;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Decode Opus, write to device speakers (INPUT path hot function)
|
|
* Processing pipeline: Opus decode (with FEC) → ALSA playback with error recovery
|
|
* @param opus_buf Encoded Opus packet from client
|
|
* @param opus_size Size of Opus packet in bytes
|
|
* @return >0 = PCM frames written, 0 = frame skipped, -1/-2 = error
|
|
*/
|
|
__attribute__((hot)) int jetkvm_audio_decode_write(void * __restrict__ opus_buf, int32_t opus_size) {
|
|
static short CACHE_ALIGN pcm_buffer[960 * 2]; // Cache-aligned
|
|
unsigned char * __restrict__ in = (unsigned char*)opus_buf;
|
|
int32_t pcm_frames, pcm_rc, err = 0;
|
|
uint8_t recovery_attempts = 0;
|
|
const uint8_t max_recovery_attempts = 3;
|
|
|
|
// Prefetch input buffer - locality 0 for immediate use
|
|
SIMD_PREFETCH(in, 0, 0);
|
|
|
|
if (__builtin_expect(!playback_initialized || !pcm_playback_handle || !decoder || !opus_buf || opus_size <= 0, 0)) {
|
|
TRACE_LOG("[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;
|
|
}
|
|
|
|
if (opus_size > max_packet_size) {
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Opus packet too large - size=%d, max=%d\n", opus_size, max_packet_size);
|
|
return -1;
|
|
}
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Processing Opus packet - size=%d bytes\n", opus_size);
|
|
|
|
// Decode Opus packet to PCM (FEC automatically applied if embedded in packet)
|
|
// decode_fec=0 means normal decode (FEC data is used automatically when present)
|
|
pcm_frames = opus_decode(decoder, in, opus_size, pcm_buffer, frame_size, 0);
|
|
|
|
if (__builtin_expect(pcm_frames < 0, 0)) {
|
|
// Decode failed - attempt packet loss concealment using FEC from previous packet
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Opus decode failed with error %d, attempting packet loss concealment\n", pcm_frames);
|
|
|
|
// decode_fec=1 means use FEC data from the NEXT packet to reconstruct THIS lost packet
|
|
pcm_frames = opus_decode(decoder, NULL, 0, pcm_buffer, frame_size, 1);
|
|
if (pcm_frames < 0) {
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Packet loss concealment also failed with error %d\n", pcm_frames);
|
|
return -1;
|
|
}
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Packet loss concealment succeeded, recovered %d frames\n", pcm_frames);
|
|
} else
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Opus decode successful - decoded %d PCM frames\n", pcm_frames);
|
|
|
|
retry_write:
|
|
// Write decoded PCM to ALSA playback device
|
|
pcm_rc = snd_pcm_writei(pcm_playback_handle, pcm_buffer, pcm_frames);
|
|
if (__builtin_expect(pcm_rc < 0, 0)) {
|
|
TRACE_LOG("[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) {
|
|
recovery_attempts++;
|
|
if (recovery_attempts > max_recovery_attempts) {
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Buffer underrun recovery failed after %d attempts\n", max_recovery_attempts);
|
|
return -2;
|
|
}
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Buffer underrun detected, attempting recovery (attempt %d)\n", recovery_attempts);
|
|
err = snd_pcm_prepare(pcm_playback_handle);
|
|
if (err < 0) {
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: snd_pcm_prepare failed (%s), trying drop+prepare\n", snd_strerror(err));
|
|
snd_pcm_drop(pcm_playback_handle);
|
|
err = snd_pcm_prepare(pcm_playback_handle);
|
|
if (err < 0) {
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: drop+prepare recovery failed (%s)\n", snd_strerror(err));
|
|
return -2;
|
|
}
|
|
}
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Buffer underrun recovery successful, retrying write\n");
|
|
goto retry_write;
|
|
} else if (pcm_rc == -ESTRPIPE) {
|
|
recovery_attempts++;
|
|
if (recovery_attempts > max_recovery_attempts) {
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device suspend recovery failed after %d attempts\n", max_recovery_attempts);
|
|
return -2;
|
|
}
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device suspended, attempting resume (attempt %d)\n", recovery_attempts);
|
|
uint8_t resume_attempts = 0;
|
|
while ((err = snd_pcm_resume(pcm_playback_handle)) == -EAGAIN && resume_attempts < 10) {
|
|
snd_pcm_wait(pcm_playback_handle, sleep_milliseconds);
|
|
resume_attempts++;
|
|
}
|
|
if (err < 0) {
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device resume failed (%s), trying prepare fallback\n", snd_strerror(err));
|
|
err = snd_pcm_prepare(pcm_playback_handle);
|
|
if (err < 0) {
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Prepare fallback failed (%s)\n", snd_strerror(err));
|
|
return -2;
|
|
}
|
|
}
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device suspend recovery successful, skipping frame\n");
|
|
return 0;
|
|
} else if (pcm_rc == -ENODEV) {
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device disconnected (ENODEV) - critical error\n");
|
|
return -2;
|
|
} else if (pcm_rc == -EIO) {
|
|
recovery_attempts++;
|
|
if (recovery_attempts <= max_recovery_attempts) {
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: I/O error detected, attempting recovery\n");
|
|
snd_pcm_drop(pcm_playback_handle);
|
|
err = snd_pcm_prepare(pcm_playback_handle);
|
|
if (err >= 0) {
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: I/O error recovery successful, retrying write\n");
|
|
goto retry_write;
|
|
}
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: I/O error recovery failed (%s)\n", snd_strerror(err));
|
|
}
|
|
return -2;
|
|
} else if (pcm_rc == -EAGAIN) {
|
|
recovery_attempts++;
|
|
if (recovery_attempts <= max_recovery_attempts) {
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device not ready (EAGAIN), waiting and retrying\n");
|
|
snd_pcm_wait(pcm_playback_handle, 1); // Wait 1ms
|
|
goto retry_write;
|
|
}
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Device not ready recovery failed after %d attempts\n", max_recovery_attempts);
|
|
return -2;
|
|
} else {
|
|
recovery_attempts++;
|
|
if (recovery_attempts <= 1 && (pcm_rc == -EINTR || pcm_rc == -EBUSY)) {
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Transient error %d (%s), retrying once\n", pcm_rc, snd_strerror(pcm_rc));
|
|
snd_pcm_wait(pcm_playback_handle, 1); // Wait 1ms
|
|
goto retry_write;
|
|
}
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Unrecoverable error %d (%s)\n", pcm_rc, snd_strerror(pcm_rc));
|
|
return -2;
|
|
}
|
|
}
|
|
TRACE_LOG("[AUDIO_INPUT] jetkvm_audio_decode_write: Successfully wrote %d PCM frames to device\n", pcm_frames);
|
|
return pcm_frames;
|
|
}
|
|
|
|
// CLEANUP FUNCTIONS
|
|
|
|
/**
|
|
* Close INPUT path (thread-safe with drain)
|
|
*/
|
|
void jetkvm_audio_playback_close() {
|
|
while (playback_initializing) {
|
|
sched_yield();
|
|
}
|
|
|
|
if (__sync_bool_compare_and_swap(&playback_initialized, 1, 0) == 0) {
|
|
return;
|
|
}
|
|
|
|
if (decoder) {
|
|
opus_decoder_destroy(decoder);
|
|
decoder = NULL;
|
|
}
|
|
if (pcm_playback_handle) {
|
|
snd_pcm_drain(pcm_playback_handle);
|
|
snd_pcm_close(pcm_playback_handle);
|
|
pcm_playback_handle = NULL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close OUTPUT path (thread-safe with drain)
|
|
*/
|
|
void jetkvm_audio_capture_close() {
|
|
while (capture_initializing) {
|
|
sched_yield();
|
|
}
|
|
|
|
if (__sync_bool_compare_and_swap(&capture_initialized, 1, 0) == 0) {
|
|
return;
|
|
}
|
|
|
|
if (encoder) {
|
|
opus_encoder_destroy(encoder);
|
|
encoder = NULL;
|
|
}
|
|
if (pcm_capture_handle) {
|
|
snd_pcm_drain(pcm_capture_handle);
|
|
snd_pcm_close(pcm_capture_handle);
|
|
pcm_capture_handle = NULL;
|
|
}
|
|
}
|