kvm/internal/audio/cgo_audio.go

488 lines
13 KiB
Go

//go:build cgo
package audio
import (
"errors"
"unsafe"
)
/*
#cgo CFLAGS: -I${SRCDIR}/../../tools/alsa-opus-includes
#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;
static int opus_bitrate = 64000;
static int opus_complexity = 5;
static int sample_rate = 48000;
static int channels = 2;
static int frame_size = 960; // 20ms for 48kHz
static int max_packet_size = 1500;
// 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(50000); // 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(&params);
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
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;
}
opus_encoder_ctl(encoder, OPUS_SET_BITRATE(opus_bitrate));
opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(opus_complexity));
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 {
ret := C.jetkvm_audio_init()
if ret != 0 {
return errAudioInitFailed
}
return nil
}
func cgoAudioClose() {
C.jetkvm_audio_close()
}
// Optimized read and encode with pre-allocated error objects and reduced checks
func cgoAudioReadEncode(buf []byte) (int, error) {
// Fast path: check minimum buffer size (reduced from 1500 to 1276 for 10ms frames)
if len(buf) < 1276 {
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
}
// Go wrappers for audio playback (microphone input)
func cgoAudioPlaybackInit() error {
ret := C.jetkvm_audio_playback_init()
if ret != 0 {
return errors.New("failed to init ALSA playback/Opus decoder")
}
return nil
}
func cgoAudioPlaybackClose() {
C.jetkvm_audio_playback_close()
}
// Decodes Opus frame and writes to playback device
func cgoAudioDecodeWrite(buf []byte) (int, error) {
if len(buf) == 0 {
return 0, errors.New("empty buffer")
}
// Additional safety check to prevent segfault
if buf == nil {
return 0, errors.New("nil buffer")
}
// Validate buffer size to prevent potential overruns
if len(buf) > 4096 { // Maximum reasonable Opus frame size
return 0, errors.New("buffer too large")
}
// Ensure buffer is not deallocated by keeping a reference
bufPtr := unsafe.Pointer(&buf[0])
if bufPtr == nil {
return 0, errors.New("invalid buffer pointer")
}
// Add recovery mechanism for C function crashes
defer func() {
if r := recover(); r != nil {
// Log the panic but don't crash the entire program
// This should not happen with proper validation, but provides safety
}
}()
n := C.jetkvm_audio_decode_write(bufPtr, C.int(len(buf)))
if n < 0 {
return 0, errors.New("audio decode/write error")
}
return int(n), nil
}
// Wrapper functions for non-blocking audio manager
var (
CGOAudioInit = cgoAudioInit
CGOAudioClose = cgoAudioClose
CGOAudioReadEncode = cgoAudioReadEncode
CGOAudioPlaybackInit = cgoAudioPlaybackInit
CGOAudioPlaybackClose = cgoAudioPlaybackClose
CGOAudioDecodeWrite = cgoAudioDecodeWrite
)