mirror of
https://github.com/jetkvm/kvm.git
synced 2025-09-16 08:38:14 +00:00
Replace hardcoded values with centralized config constants for better maintainability and flexibility. This includes sleep durations, buffer sizes, thresholds, and various audio processing parameters. The changes affect multiple components including buffer pools, latency monitoring, IPC, and audio processing. This refactoring makes it easier to adjust parameters without modifying individual files. Key changes: - Replace hardcoded sleep durations with config values - Centralize buffer sizes and pool configurations - Move thresholds and limits to config - Update audio quality presets to use config values
531 lines
15 KiB
Go
531 lines
15 KiB
Go
//go:build cgo
|
|
|
|
package audio
|
|
|
|
import (
|
|
"errors"
|
|
"unsafe"
|
|
)
|
|
|
|
/*
|
|
#cgo CFLAGS: -I$HOME/.jetkvm/audio-libs/alsa-lib-$ALSA_VERSION/include -I$HOME/.jetkvm/audio-libs/opus-$OPUS_VERSION/include -I$HOME/.jetkvm/audio-libs/opus-$OPUS_VERSION/celt
|
|
#cgo LDFLAGS: -L$HOME/.jetkvm/audio-libs/alsa-lib-$ALSA_VERSION/src/.libs -lasound -L$HOME/.jetkvm/audio-libs/opus-$OPUS_VERSION/.libs -lopus -lm -ldl -static
|
|
#include <alsa/asoundlib.h>
|
|
#include <opus.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
|
|
// C state for ALSA/Opus with safety flags
|
|
static snd_pcm_t *pcm_handle = NULL;
|
|
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 GetConfig().CGOOpusBitrate
|
|
static int opus_complexity = 3; // Will be set from GetConfig().CGOOpusComplexity
|
|
static int opus_vbr = 1; // Will be set from GetConfig().CGOOpusVBR
|
|
static int opus_vbr_constraint = 1; // Will be set from GetConfig().CGOOpusVBRConstraint
|
|
static int opus_signal_type = 3; // Will be set from GetConfig().CGOOpusSignalType
|
|
static int opus_bandwidth = 1105; // Will be set from GetConfig().CGOOpusBandwidth
|
|
static int opus_dtx = 0; // Will be set from GetConfig().CGOOpusDTX
|
|
static int sample_rate = 48000; // Will be set from GetConfig().CGOSampleRate
|
|
static int channels = 2; // Will be set from GetConfig().CGOChannels
|
|
static int frame_size = 960; // Will be set from GetConfig().CGOFrameSize
|
|
static int max_packet_size = 1500; // Will be set from GetConfig().CGOMaxPacketSize
|
|
static int sleep_microseconds = 50000; // Will be set from GetConfig().CGOSleepMicroseconds
|
|
|
|
// Function to update constants from Go configuration
|
|
void update_audio_constants(int bitrate, int complexity, int vbr, int vbr_constraint,
|
|
int signal_type, int bandwidth, int dtx, int sr, int ch,
|
|
int fs, int max_pkt, int sleep_us) {
|
|
opus_bitrate = bitrate;
|
|
opus_complexity = complexity;
|
|
opus_vbr = vbr;
|
|
opus_vbr_constraint = vbr_constraint;
|
|
opus_signal_type = signal_type;
|
|
opus_bandwidth = bandwidth;
|
|
opus_dtx = dtx;
|
|
sample_rate = sr;
|
|
channels = ch;
|
|
frame_size = fs;
|
|
max_packet_size = max_pkt;
|
|
sleep_microseconds = sleep_us;
|
|
}
|
|
|
|
// State tracking to prevent race conditions during rapid start/stop
|
|
static volatile int capture_initializing = 0;
|
|
static volatile int capture_initialized = 0;
|
|
static volatile int playback_initializing = 0;
|
|
static volatile int playback_initialized = 0;
|
|
|
|
// Safe ALSA device opening with retry logic
|
|
static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream_t stream) {
|
|
int attempts = 3;
|
|
int err;
|
|
|
|
while (attempts-- > 0) {
|
|
err = snd_pcm_open(handle, device, stream, SND_PCM_NONBLOCK);
|
|
if (err >= 0) {
|
|
// Switch to blocking mode after successful open
|
|
snd_pcm_nonblock(*handle, 0);
|
|
return 0;
|
|
}
|
|
|
|
if (err == -EBUSY && attempts > 0) {
|
|
// Device busy, wait and retry
|
|
usleep(sleep_microseconds); // 50ms
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
return err;
|
|
}
|
|
|
|
// Optimized ALSA configuration with stack allocation and performance tuning
|
|
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;
|
|
|
|
// Use stack allocation for better performance
|
|
snd_pcm_hw_params_alloca(¶ms);
|
|
snd_pcm_sw_params_alloca(&sw_params);
|
|
|
|
// Hardware parameters
|
|
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;
|
|
|
|
// Set exact rate for better performance
|
|
err = snd_pcm_hw_params_set_rate(handle, params, sample_rate, 0);
|
|
if (err < 0) {
|
|
// Fallback to near rate if exact fails
|
|
unsigned int rate = sample_rate;
|
|
err = snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0);
|
|
if (err < 0) return err;
|
|
}
|
|
|
|
// Optimize buffer sizes for low latency
|
|
snd_pcm_uframes_t period_size = frame_size;
|
|
err = snd_pcm_hw_params_set_period_size_near(handle, params, &period_size, 0);
|
|
if (err < 0) return err;
|
|
|
|
// Set buffer size to 4 periods for good latency/stability balance
|
|
snd_pcm_uframes_t buffer_size = period_size * 4;
|
|
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;
|
|
|
|
// Software parameters for optimal performance
|
|
err = snd_pcm_sw_params_current(handle, sw_params);
|
|
if (err < 0) return err;
|
|
|
|
// Start playback/capture when buffer is period_size frames
|
|
err = snd_pcm_sw_params_set_start_threshold(handle, sw_params, period_size);
|
|
if (err < 0) return err;
|
|
|
|
// Allow transfers when at least period_size frames are available
|
|
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);
|
|
}
|
|
|
|
// Initialize ALSA and Opus encoder with improved safety
|
|
int jetkvm_audio_init() {
|
|
int err;
|
|
|
|
// Prevent concurrent initialization
|
|
if (__sync_bool_compare_and_swap(&capture_initializing, 0, 1) == 0) {
|
|
return -EBUSY; // Already initializing
|
|
}
|
|
|
|
// Check if already initialized
|
|
if (capture_initialized) {
|
|
capture_initializing = 0;
|
|
return 0;
|
|
}
|
|
|
|
// Clean up any existing resources first
|
|
if (encoder) {
|
|
opus_encoder_destroy(encoder);
|
|
encoder = NULL;
|
|
}
|
|
if (pcm_handle) {
|
|
snd_pcm_close(pcm_handle);
|
|
pcm_handle = NULL;
|
|
}
|
|
|
|
// Try to open ALSA capture device
|
|
err = safe_alsa_open(&pcm_handle, "hw:1,0", SND_PCM_STREAM_CAPTURE);
|
|
if (err < 0) {
|
|
capture_initializing = 0;
|
|
return -1;
|
|
}
|
|
|
|
// Configure the device
|
|
err = configure_alsa_device(pcm_handle, "capture");
|
|
if (err < 0) {
|
|
snd_pcm_close(pcm_handle);
|
|
pcm_handle = NULL;
|
|
capture_initializing = 0;
|
|
return -1;
|
|
}
|
|
|
|
// Initialize Opus encoder with optimized settings
|
|
int opus_err = 0;
|
|
encoder = opus_encoder_create(sample_rate, channels, OPUS_APPLICATION_AUDIO, &opus_err);
|
|
if (!encoder || opus_err != OPUS_OK) {
|
|
if (pcm_handle) { snd_pcm_close(pcm_handle); pcm_handle = NULL; }
|
|
capture_initializing = 0;
|
|
return -2;
|
|
}
|
|
|
|
// Apply optimized Opus encoder 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));
|
|
// Enable packet loss concealment for better resilience
|
|
opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(5));
|
|
// Set prediction disabled for lower latency
|
|
opus_encoder_ctl(encoder, OPUS_SET_PREDICTION_DISABLED(1));
|
|
|
|
capture_initialized = 1;
|
|
capture_initializing = 0;
|
|
return 0;
|
|
}
|
|
|
|
// Read and encode one frame with enhanced error handling
|
|
int jetkvm_audio_read_encode(void *opus_buf) {
|
|
short pcm_buffer[1920]; // max 2ch*960
|
|
unsigned char *out = (unsigned char*)opus_buf;
|
|
int err = 0;
|
|
|
|
// Safety checks
|
|
if (!capture_initialized || !pcm_handle || !encoder || !opus_buf) {
|
|
return -1;
|
|
}
|
|
|
|
int pcm_rc = snd_pcm_readi(pcm_handle, pcm_buffer, frame_size);
|
|
|
|
// Handle ALSA errors with enhanced recovery
|
|
if (pcm_rc < 0) {
|
|
if (pcm_rc == -EPIPE) {
|
|
// Buffer underrun - try to recover
|
|
err = snd_pcm_prepare(pcm_handle);
|
|
if (err < 0) return -1;
|
|
|
|
pcm_rc = snd_pcm_readi(pcm_handle, pcm_buffer, frame_size);
|
|
if (pcm_rc < 0) return -1;
|
|
} else if (pcm_rc == -EAGAIN) {
|
|
// No data available - return 0 to indicate no frame
|
|
return 0;
|
|
} else if (pcm_rc == -ESTRPIPE) {
|
|
// Device suspended, try to resume
|
|
while ((err = snd_pcm_resume(pcm_handle)) == -EAGAIN) {
|
|
usleep(1000); // 1ms
|
|
}
|
|
if (err < 0) {
|
|
err = snd_pcm_prepare(pcm_handle);
|
|
if (err < 0) return -1;
|
|
}
|
|
return 0; // Skip this frame
|
|
} else {
|
|
// Other error - return error code
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// If we got fewer frames than expected, pad with silence
|
|
if (pcm_rc < frame_size) {
|
|
memset(&pcm_buffer[pcm_rc * channels], 0, (frame_size - pcm_rc) * channels * sizeof(short));
|
|
}
|
|
|
|
int nb_bytes = opus_encode(encoder, pcm_buffer, frame_size, out, max_packet_size);
|
|
return nb_bytes;
|
|
}
|
|
|
|
// Initialize ALSA playback with improved safety
|
|
int jetkvm_audio_playback_init() {
|
|
int err;
|
|
|
|
// Prevent concurrent initialization
|
|
if (__sync_bool_compare_and_swap(&playback_initializing, 0, 1) == 0) {
|
|
return -EBUSY; // Already initializing
|
|
}
|
|
|
|
// Check if already initialized
|
|
if (playback_initialized) {
|
|
playback_initializing = 0;
|
|
return 0;
|
|
}
|
|
|
|
// Clean up any existing resources first
|
|
if (decoder) {
|
|
opus_decoder_destroy(decoder);
|
|
decoder = NULL;
|
|
}
|
|
if (pcm_playback_handle) {
|
|
snd_pcm_close(pcm_playback_handle);
|
|
pcm_playback_handle = NULL;
|
|
}
|
|
|
|
// Try to open the USB gadget audio device for playback
|
|
err = safe_alsa_open(&pcm_playback_handle, "hw:1,0", SND_PCM_STREAM_PLAYBACK);
|
|
if (err < 0) {
|
|
// Fallback to default device
|
|
err = safe_alsa_open(&pcm_playback_handle, "default", SND_PCM_STREAM_PLAYBACK);
|
|
if (err < 0) {
|
|
playback_initializing = 0;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// Configure the device
|
|
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;
|
|
}
|
|
|
|
// Initialize Opus decoder
|
|
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 and write PCM with enhanced error handling
|
|
int jetkvm_audio_decode_write(void *opus_buf, int opus_size) {
|
|
short pcm_buffer[1920]; // max 2ch*960
|
|
unsigned char *in = (unsigned char*)opus_buf;
|
|
int err = 0;
|
|
|
|
// Safety checks
|
|
if (!playback_initialized || !pcm_playback_handle || !decoder || !opus_buf || opus_size <= 0) {
|
|
return -1;
|
|
}
|
|
|
|
// Additional bounds checking
|
|
if (opus_size > max_packet_size) {
|
|
return -1;
|
|
}
|
|
|
|
// Decode Opus to PCM
|
|
int pcm_frames = opus_decode(decoder, in, opus_size, pcm_buffer, frame_size, 0);
|
|
if (pcm_frames < 0) return -1;
|
|
|
|
// Write PCM to playback device with enhanced recovery
|
|
int pcm_rc = snd_pcm_writei(pcm_playback_handle, pcm_buffer, pcm_frames);
|
|
if (pcm_rc < 0) {
|
|
if (pcm_rc == -EPIPE) {
|
|
// Buffer underrun - try to recover
|
|
err = snd_pcm_prepare(pcm_playback_handle);
|
|
if (err < 0) return -2;
|
|
|
|
pcm_rc = snd_pcm_writei(pcm_playback_handle, pcm_buffer, pcm_frames);
|
|
} else if (pcm_rc == -ESTRPIPE) {
|
|
// Device suspended, try to resume
|
|
while ((err = snd_pcm_resume(pcm_playback_handle)) == -EAGAIN) {
|
|
usleep(1000); // 1ms
|
|
}
|
|
if (err < 0) {
|
|
err = snd_pcm_prepare(pcm_playback_handle);
|
|
if (err < 0) return -2;
|
|
}
|
|
return 0; // Skip this frame
|
|
}
|
|
if (pcm_rc < 0) return -2;
|
|
}
|
|
|
|
return pcm_frames;
|
|
}
|
|
|
|
// Safe playback cleanup with double-close protection
|
|
void jetkvm_audio_playback_close() {
|
|
// Wait for any ongoing operations to complete
|
|
while (playback_initializing) {
|
|
usleep(1000); // 1ms
|
|
}
|
|
|
|
// Atomic check and set to prevent double cleanup
|
|
if (__sync_bool_compare_and_swap(&playback_initialized, 1, 0) == 0) {
|
|
return; // Already cleaned up
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Safe capture cleanup
|
|
void jetkvm_audio_close() {
|
|
// Wait for any ongoing operations to complete
|
|
while (capture_initializing) {
|
|
usleep(1000); // 1ms
|
|
}
|
|
|
|
capture_initialized = 0;
|
|
|
|
if (encoder) {
|
|
opus_encoder_destroy(encoder);
|
|
encoder = NULL;
|
|
}
|
|
if (pcm_handle) {
|
|
snd_pcm_drop(pcm_handle); // Drop pending samples
|
|
snd_pcm_close(pcm_handle);
|
|
pcm_handle = NULL;
|
|
}
|
|
|
|
// Also clean up playback
|
|
jetkvm_audio_playback_close();
|
|
}
|
|
*/
|
|
import "C"
|
|
|
|
// Optimized Go wrappers with reduced overhead
|
|
var (
|
|
errAudioInitFailed = errors.New("failed to init ALSA/Opus")
|
|
errBufferTooSmall = errors.New("buffer too small")
|
|
errAudioReadEncode = errors.New("audio read/encode error")
|
|
errAudioDecodeWrite = errors.New("audio decode/write error")
|
|
errAudioPlaybackInit = errors.New("failed to init ALSA playback/Opus decoder")
|
|
errEmptyBuffer = errors.New("empty buffer")
|
|
errNilBuffer = errors.New("nil buffer")
|
|
errBufferTooLarge = errors.New("buffer too large")
|
|
errInvalidBufferPtr = errors.New("invalid buffer pointer")
|
|
)
|
|
|
|
func cgoAudioInit() error {
|
|
// Update C constants from Go configuration
|
|
config := GetConfig()
|
|
C.update_audio_constants(
|
|
C.int(config.CGOOpusBitrate),
|
|
C.int(config.CGOOpusComplexity),
|
|
C.int(config.CGOOpusVBR),
|
|
C.int(config.CGOOpusVBRConstraint),
|
|
C.int(config.CGOOpusSignalType),
|
|
C.int(config.CGOOpusBandwidth),
|
|
C.int(config.CGOOpusDTX),
|
|
C.int(config.CGOSampleRate),
|
|
C.int(config.CGOChannels),
|
|
C.int(config.CGOFrameSize),
|
|
C.int(config.CGOMaxPacketSize),
|
|
C.int(config.CGOSleepMicroseconds),
|
|
)
|
|
|
|
result := C.jetkvm_audio_init()
|
|
if result != 0 {
|
|
return errAudioInitFailed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cgoAudioClose() {
|
|
C.jetkvm_audio_close()
|
|
}
|
|
|
|
func cgoAudioReadEncode(buf []byte) (int, error) {
|
|
if len(buf) < GetConfig().MinReadEncodeBuffer {
|
|
return 0, errBufferTooSmall
|
|
}
|
|
|
|
n := C.jetkvm_audio_read_encode(unsafe.Pointer(&buf[0]))
|
|
if n < 0 {
|
|
return 0, errAudioReadEncode
|
|
}
|
|
if n == 0 {
|
|
return 0, nil // No data available
|
|
}
|
|
return int(n), nil
|
|
}
|
|
|
|
// Audio playback functions
|
|
func cgoAudioPlaybackInit() error {
|
|
ret := C.jetkvm_audio_playback_init()
|
|
if ret != 0 {
|
|
return errAudioPlaybackInit
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cgoAudioPlaybackClose() {
|
|
C.jetkvm_audio_playback_close()
|
|
}
|
|
|
|
func cgoAudioDecodeWrite(buf []byte) (int, error) {
|
|
if len(buf) == 0 {
|
|
return 0, errEmptyBuffer
|
|
}
|
|
if buf == nil {
|
|
return 0, errNilBuffer
|
|
}
|
|
if len(buf) > GetConfig().MaxDecodeWriteBuffer {
|
|
return 0, errBufferTooLarge
|
|
}
|
|
|
|
bufPtr := unsafe.Pointer(&buf[0])
|
|
if bufPtr == nil {
|
|
return 0, errInvalidBufferPtr
|
|
}
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
_ = r
|
|
}
|
|
}()
|
|
|
|
n := C.jetkvm_audio_decode_write(bufPtr, C.int(len(buf)))
|
|
if n < 0 {
|
|
return 0, errAudioDecodeWrite
|
|
}
|
|
return int(n), nil
|
|
}
|
|
|
|
// CGO function aliases
|
|
var (
|
|
CGOAudioInit = cgoAudioInit
|
|
CGOAudioClose = cgoAudioClose
|
|
CGOAudioReadEncode = cgoAudioReadEncode
|
|
CGOAudioPlaybackInit = cgoAudioPlaybackInit
|
|
CGOAudioPlaybackClose = cgoAudioPlaybackClose
|
|
CGOAudioDecodeWrite = cgoAudioDecodeWrite
|
|
)
|