Integrate libspeexdsp for high-quality audio resampling

Replace ALSA plugin layer resampling with libspeexdsp for improved audio
quality and reliability. This implementation uses direct hardware access
(hw:) instead of ALSA plugins (plughw:) and handles sample rate conversion
with SpeexDSP's high-quality sinc-based resampler.

Key changes:
- Add libspeexdsp 1.2.1 with ARM NEON optimizations to build dependencies
- Switch from plughw: to hw: device access for lower latency
- Implement conditional resampling (only when hardware rate ≠ 48kHz)
- Use SPEEX_RESAMPLER_QUALITY_DESKTOP for high-quality interpolation
- Add automatic audio dependency building in dev_deploy.sh

Quality improvements:
- Fix race condition in resampler cleanup with mutex protection
- Fix memory leak on resampler re-initialization
- Add buffer overflow validation (3840 frame limit for 192kHz)
- Improve error logging for resampling, encoding, and ALSA configuration
- Simplify code structure while maintaining all functionality

Technical details:
- Hardware negotiates actual sample rate (e.g., HDMI may vary)
- SpeexDSP converts hardware rate → 48kHz for Opus encoding
- USB Audio Gadget hardcoded to 48kHz (no resampling overhead)
- Static buffer allocation for zero allocation in hot path
- WebRTC requires 48kHz RTP clock rate per RFC 7587
This commit is contained in:
Alex P 2025-11-21 16:15:17 +02:00
parent 0be9dbcc6c
commit 9d86b02e66
9 changed files with 265 additions and 157 deletions

View File

@ -15,6 +15,7 @@ function use_sudo() {
# Accept version parameters or use defaults # Accept version parameters or use defaults
ALSA_VERSION="${1:-1.2.14}" ALSA_VERSION="${1:-1.2.14}"
OPUS_VERSION="${2:-1.5.2}" OPUS_VERSION="${2:-1.5.2}"
SPEEXDSP_VERSION="${3:-1.2.1}"
AUDIO_LIBS_DIR="/opt/jetkvm-audio-libs" AUDIO_LIBS_DIR="/opt/jetkvm-audio-libs"
BUILDKIT_PATH="/opt/jetkvm-native-buildkit" BUILDKIT_PATH="/opt/jetkvm-native-buildkit"
@ -29,12 +30,14 @@ cd "$AUDIO_LIBS_DIR"
# Download sources # Download sources
[ -f alsa-lib-${ALSA_VERSION}.tar.bz2 ] || wget -N https://www.alsa-project.org/files/pub/lib/alsa-lib-${ALSA_VERSION}.tar.bz2 [ -f alsa-lib-${ALSA_VERSION}.tar.bz2 ] || wget -N https://www.alsa-project.org/files/pub/lib/alsa-lib-${ALSA_VERSION}.tar.bz2
[ -f opus-${OPUS_VERSION}.tar.gz ] || wget -N https://downloads.xiph.org/releases/opus/opus-${OPUS_VERSION}.tar.gz [ -f opus-${OPUS_VERSION}.tar.gz ] || wget -N https://downloads.xiph.org/releases/opus/opus-${OPUS_VERSION}.tar.gz
[ -f speexdsp-${SPEEXDSP_VERSION}.tar.gz ] || wget -N https://ftp.osuosl.org/pub/xiph/releases/speex/speexdsp-${SPEEXDSP_VERSION}.tar.gz
# Extract # Extract
[ -d alsa-lib-${ALSA_VERSION} ] || tar xf alsa-lib-${ALSA_VERSION}.tar.bz2 [ -d alsa-lib-${ALSA_VERSION} ] || tar xf alsa-lib-${ALSA_VERSION}.tar.bz2
[ -d opus-${OPUS_VERSION} ] || tar xf opus-${OPUS_VERSION}.tar.gz [ -d opus-${OPUS_VERSION} ] || tar xf opus-${OPUS_VERSION}.tar.gz
[ -d speexdsp-${SPEEXDSP_VERSION} ] || tar xf speexdsp-${SPEEXDSP_VERSION}.tar.gz
# Optimization flags for ARM Cortex-A7 with NEON (simplified to avoid FD_SETSIZE issues) # ARM Cortex-A7 optimization flags with NEON support
OPTIM_CFLAGS="-O2 -mfpu=neon -mtune=cortex-a7 -mfloat-abi=hard" OPTIM_CFLAGS="-O2 -mfpu=neon -mtune=cortex-a7 -mfloat-abi=hard"
export CC="${CROSS_PREFIX}-gcc" export CC="${CROSS_PREFIX}-gcc"
@ -45,7 +48,7 @@ export CXXFLAGS="$OPTIM_CFLAGS"
cd alsa-lib-${ALSA_VERSION} cd alsa-lib-${ALSA_VERSION}
if [ ! -f .built ]; then if [ ! -f .built ]; then
chown -R $(whoami):$(whoami) . chown -R $(whoami):$(whoami) .
# Use minimal ALSA configuration to avoid FD_SETSIZE issues in devcontainer # Minimal ALSA configuration for audio capture/playback
CFLAGS="$OPTIM_CFLAGS" ./configure --host $BUILDKIT_FLAVOR \ CFLAGS="$OPTIM_CFLAGS" ./configure --host $BUILDKIT_FLAVOR \
--enable-static=yes --enable-shared=no \ --enable-static=yes --enable-shared=no \
--with-pcm-plugins=plug,rate,linear,copy \ --with-pcm-plugins=plug,rate,linear,copy \
@ -68,4 +71,18 @@ if [ ! -f .built ]; then
fi fi
cd .. cd ..
echo "ALSA and Opus built in $AUDIO_LIBS_DIR" # Build SpeexDSP
cd speexdsp-${SPEEXDSP_VERSION}
if [ ! -f .built ]; then
chown -R $(whoami):$(whoami) .
# NEON-optimized high-quality resampler
CFLAGS="$OPTIM_CFLAGS" ./configure --host $BUILDKIT_FLAVOR \
--enable-static=yes --enable-shared=no \
--enable-neon \
--disable-examples
make -j$(nproc)
touch .built
fi
cd ..
echo "ALSA, Opus, and SpeexDSP built in $AUDIO_LIBS_DIR"

View File

@ -30,9 +30,9 @@ var (
func getAlsaDevice(source string) string { func getAlsaDevice(source string) string {
if source == "hdmi" { if source == "hdmi" {
return "plughw:0,0" return "hw:0,0" // TC358743 HDMI audio
} }
return "plughw:1,0" return "hw:1,0" // USB Audio Gadget
} }
func initAudio() { func initAudio() {
@ -67,15 +67,6 @@ func getAudioConfig() audio.AudioConfig {
audioLogger.Warn().Int("buffer_periods", config.AudioBufferPeriods).Msg("Invalid buffer periods, using default") audioLogger.Warn().Int("buffer_periods", config.AudioBufferPeriods).Msg("Invalid buffer periods, using default")
} }
switch config.AudioSampleRate {
case 8000, 12000, 16000, 24000, 48000:
cfg.SampleRate = uint32(config.AudioSampleRate)
default:
if config.AudioSampleRate != 0 {
audioLogger.Warn().Int("sample_rate", config.AudioSampleRate).Msg("Invalid sample rate, using default")
}
}
if config.AudioPacketLossPerc >= 0 && config.AudioPacketLossPerc <= 100 { if config.AudioPacketLossPerc >= 0 && config.AudioPacketLossPerc <= 100 {
cfg.PacketLossPerc = uint8(config.AudioPacketLossPerc) cfg.PacketLossPerc = uint8(config.AudioPacketLossPerc)
} else if config.AudioPacketLossPerc != 0 { } else if config.AudioPacketLossPerc != 0 {
@ -105,22 +96,29 @@ func startAudio() error {
ensureConfigLoaded() ensureConfigLoaded()
var outputErr, inputErr error var outputErr, inputErr error
// Start output audio if enabled and track is available
if audioOutputEnabled.Load() && currentAudioTrack != nil { if audioOutputEnabled.Load() && currentAudioTrack != nil {
outputErr = startOutputAudioUnderMutex(getAlsaDevice(config.AudioOutputSource)) outputErr = startOutputAudioUnderMutex(getAlsaDevice(config.AudioOutputSource))
} }
// Start input audio if enabled and USB audio device is configured
if audioInputEnabled.Load() && config.UsbDevices != nil && config.UsbDevices.Audio { if audioInputEnabled.Load() && config.UsbDevices != nil && config.UsbDevices.Audio {
inputErr = startInputAudioUnderMutex(getAlsaDevice("usb")) inputErr = startInputAudioUnderMutex(getAlsaDevice("usb"))
} }
if outputErr != nil || inputErr != nil { // Return combined errors if any
if outputErr != nil && inputErr != nil { if outputErr != nil && inputErr != nil {
return fmt.Errorf("audio start failed - output: %w, input: %v", outputErr, inputErr) return fmt.Errorf("audio start failed - output: %w, input: %v", outputErr, inputErr)
}
return firstError(outputErr, inputErr)
}
func firstError(errs ...error) error {
for _, err := range errs {
if err != nil {
return err
} }
if outputErr != nil {
return outputErr
}
return inputErr
} }
return nil return nil
} }
@ -291,15 +289,8 @@ func SetAudioInputEnabled(enabled bool) error {
return nil return nil
} }
// SetAudioOutputSource switches between HDMI (hw:0,0) and USB (hw:1,0) audio capture. // SetAudioOutputSource switches between HDMI and USB audio capture.
// // Config is saved synchronously, audio restarts asynchronously.
// The function returns immediately after updating and persisting the config change,
// while the actual audio device switch happens asynchronously in the background:
// - Config save is synchronous to ensure the change persists even if the process crashes
// - Audio restart is async to avoid blocking the RPC caller during ALSA reconfiguration
//
// Note: The HDMI audio device (hw:0,0) can take 30-60 seconds to initialize due to
// TC358743 hardware characteristics. Callers receive success before audio actually switches.
func SetAudioOutputSource(source string) error { func SetAudioOutputSource(source string) error {
if source != "hdmi" && source != "usb" { if source != "hdmi" && source != "usb" {
return fmt.Errorf("invalid audio source: %s (must be 'hdmi' or 'usb')", source) return fmt.Errorf("invalid audio source: %s (must be 'hdmi' or 'usb')", source)
@ -399,7 +390,6 @@ func handleInputTrackForSession(track *webrtc.TrackRemote) {
} }
} }
// processInputPacket handles writing audio data to the input source
func processInputPacket(opusData []byte) error { func processInputPacket(opusData []byte) error {
inputSourceMutex.Lock() inputSourceMutex.Lock()
defer inputSourceMutex.Unlock() defer inputSourceMutex.Unlock()
@ -409,14 +399,14 @@ func processInputPacket(opusData []byte) error {
return nil return nil
} }
// Ensure source is connected // Lazy connect on first use
if !(*source).IsConnected() { if !(*source).IsConnected() {
if err := (*source).Connect(); err != nil { if err := (*source).Connect(); err != nil {
return err return err
} }
} }
// Write the message // Write opus data, disconnect on error
if err := (*source).WriteMessage(0, opusData); err != nil { if err := (*source).WriteMessage(0, opusData); err != nil {
(*source).Disconnect() (*source).Disconnect()
return err return err

View File

@ -122,7 +122,6 @@ type Config struct {
AudioDTXEnabled bool `json:"audio_dtx_enabled"` AudioDTXEnabled bool `json:"audio_dtx_enabled"`
AudioFECEnabled bool `json:"audio_fec_enabled"` AudioFECEnabled bool `json:"audio_fec_enabled"`
AudioBufferPeriods int `json:"audio_buffer_periods"` // 2-24 AudioBufferPeriods int `json:"audio_buffer_periods"` // 2-24
AudioSampleRate int `json:"audio_sample_rate"` // Hz (Opus: 8k, 12k, 16k, 24k, 48k)
AudioPacketLossPerc int `json:"audio_packet_loss_perc"` // 0-100 AudioPacketLossPerc int `json:"audio_packet_loss_perc"` // 0-100
NativeMaxRestart uint `json:"native_max_restart_attempts"` NativeMaxRestart uint `json:"native_max_restart_attempts"`
} }
@ -218,7 +217,6 @@ func getDefaultConfig() Config {
AudioDTXEnabled: true, AudioDTXEnabled: true,
AudioFECEnabled: true, AudioFECEnabled: true,
AudioBufferPeriods: 12, AudioBufferPeriods: 12,
AudioSampleRate: 48000,
AudioPacketLossPerc: 20, AudioPacketLossPerc: 20,
} }
} }
@ -298,7 +296,6 @@ func LoadConfig() {
loadedConfig.AudioDTXEnabled = defaults.AudioDTXEnabled loadedConfig.AudioDTXEnabled = defaults.AudioDTXEnabled
loadedConfig.AudioFECEnabled = defaults.AudioFECEnabled loadedConfig.AudioFECEnabled = defaults.AudioFECEnabled
loadedConfig.AudioBufferPeriods = defaults.AudioBufferPeriods loadedConfig.AudioBufferPeriods = defaults.AudioBufferPeriods
loadedConfig.AudioSampleRate = defaults.AudioSampleRate
loadedConfig.AudioPacketLossPerc = defaults.AudioPacketLossPerc loadedConfig.AudioPacketLossPerc = defaults.AudioPacketLossPerc
} }

View File

@ -3,20 +3,22 @@
* *
* Bidirectional audio processing optimized for ARM NEON SIMD: * Bidirectional audio processing optimized for ARM NEON SIMD:
* - OUTPUT PATH: TC358743 HDMI or USB Gadget audio Client speakers * - OUTPUT PATH: TC358743 HDMI or USB Gadget audio Client speakers
* Pipeline: ALSA plughw:0,0 or plughw:1,0 capture Opus encode (192kbps, FEC enabled) * Pipeline: ALSA hw:0,0 or hw:1,0 capture SpeexDSP resample Opus encode (192kbps, FEC enabled)
* *
* - INPUT PATH: Client microphone Device speakers * - INPUT PATH: Client microphone Device speakers
* Pipeline: Opus decode (with FEC) ALSA plughw:1,0 playback * Pipeline: Opus decode (with FEC) ALSA hw:1,0 playback
* *
* Key features: * Key features:
* - ARM NEON SIMD optimization for all audio operations * - ARM NEON SIMD optimization for all audio operations
* - SpeexDSP high-quality resampling (SPEEX_RESAMPLER_QUALITY_DESKTOP)
* - Opus in-band FEC for packet loss resilience * - Opus in-band FEC for packet loss resilience
* - S16_LE stereo, 20ms frames (sample rate configurable: 8k/12k/16k/24k/48kHz) * - S16_LE stereo, 20ms frames at 48kHz (hardware rate auto-negotiated)
* - ALSA plughw layer provides automatic rate conversion from hardware to Opus rate * - Direct hardware access with userspace resampling (no ALSA plugin layer)
*/ */
#include <alsa/asoundlib.h> #include <alsa/asoundlib.h>
#include <opus.h> #include <opus.h>
#include <speex/speex_resampler.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -45,12 +47,15 @@ static const char *alsa_playback_device = NULL;
static OpusEncoder *encoder = NULL; static OpusEncoder *encoder = NULL;
static OpusDecoder *decoder = NULL; static OpusDecoder *decoder = NULL;
static SpeexResamplerState *capture_resampler = NULL;
// Audio format (S16_LE @ 48kHz) // Audio format - Opus always uses 48kHz for WebRTC (RFC 7587)
static uint32_t sample_rate = 48000; static const uint32_t opus_sample_rate = 48000; // Fixed: Opus RTP clock rate
static uint32_t hardware_sample_rate = 48000; // Hardware-negotiated rate
static uint8_t capture_channels = 2; // OUTPUT: Audio source (HDMI or USB) → client (stereo by default) static uint8_t capture_channels = 2; // OUTPUT: Audio source (HDMI or USB) → client (stereo by default)
static uint8_t playback_channels = 1; // INPUT: Client mono mic → device (always mono for USB audio gadget) static uint8_t playback_channels = 1; // INPUT: Client mono mic → device (always mono for USB audio gadget)
static uint16_t frame_size = 960; // 20ms frames at 48kHz static const uint16_t opus_frame_size = 960; // 20ms frames at 48kHz (fixed)
static uint16_t hardware_frame_size = 960; // 20ms frames at hardware rate
static uint32_t opus_bitrate = 192000; static uint32_t opus_bitrate = 192000;
static uint8_t opus_complexity = 8; static uint8_t opus_complexity = 8;
@ -105,34 +110,50 @@ 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 sr, uint8_t ch, uint16_t fs, uint16_t max_pkt,
uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff, uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff,
uint8_t dtx_enabled, uint8_t fec_enabled, uint8_t buf_periods, uint8_t pkt_loss_perc) { uint8_t dtx_enabled, uint8_t fec_enabled, uint8_t buf_periods, uint8_t pkt_loss_perc) {
// Validate and set bitrate (64-256 kbps range)
opus_bitrate = (bitrate >= 64000 && bitrate <= 256000) ? bitrate : 192000; opus_bitrate = (bitrate >= 64000 && bitrate <= 256000) ? bitrate : 192000;
// Set complexity (0-10 range)
opus_complexity = (complexity <= 10) ? complexity : 5; opus_complexity = (complexity <= 10) ? complexity : 5;
sample_rate = sr > 0 ? sr : 48000;
// Set channel count (mono or stereo)
capture_channels = (ch == 1 || ch == 2) ? ch : 2; capture_channels = (ch == 1 || ch == 2) ? ch : 2;
frame_size = fs > 0 ? fs : 960;
// Set packet and timing parameters
max_packet_size = max_pkt > 0 ? max_pkt : 1500; max_packet_size = max_pkt > 0 ? max_pkt : 1500;
sleep_microseconds = sleep_us > 0 ? sleep_us : 1000; sleep_microseconds = sleep_us > 0 ? sleep_us : 1000;
sleep_milliseconds = sleep_microseconds / 1000; sleep_milliseconds = sleep_microseconds / 1000;
max_attempts_global = max_attempts > 0 ? max_attempts : 5; max_attempts_global = max_attempts > 0 ? max_attempts : 5;
max_backoff_us_global = max_backoff > 0 ? max_backoff : 500000; max_backoff_us_global = max_backoff > 0 ? max_backoff : 500000;
// Set codec features
opus_dtx_enabled = dtx_enabled ? 1 : 0; opus_dtx_enabled = dtx_enabled ? 1 : 0;
opus_fec_enabled = fec_enabled ? 1 : 0; opus_fec_enabled = fec_enabled ? 1 : 0;
// Set buffer configuration
buffer_period_count = (buf_periods >= 2 && buf_periods <= 24) ? buf_periods : 12; buffer_period_count = (buf_periods >= 2 && buf_periods <= 24) ? buf_periods : 12;
opus_packet_loss_perc = (pkt_loss_perc <= 100) ? pkt_loss_perc : 20; opus_packet_loss_perc = (pkt_loss_perc <= 100) ? pkt_loss_perc : 20;
// Note: sr and fs parameters ignored - Opus always uses 48kHz with 960 samples
} }
void update_audio_decoder_constants(uint32_t sr, uint8_t ch, uint16_t fs, uint16_t max_pkt, 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, uint32_t sleep_us, uint8_t max_attempts, uint32_t max_backoff,
uint8_t buf_periods) { uint8_t buf_periods) {
sample_rate = sr > 0 ? sr : 48000; // Set playback channels (mono or stereo)
playback_channels = (ch == 1 || ch == 2) ? ch : 2; playback_channels = (ch == 1 || ch == 2) ? ch : 2;
frame_size = fs > 0 ? fs : 960;
// Set packet and timing parameters
max_packet_size = max_pkt > 0 ? max_pkt : 1500; max_packet_size = max_pkt > 0 ? max_pkt : 1500;
sleep_microseconds = sleep_us > 0 ? sleep_us : 1000; sleep_microseconds = sleep_us > 0 ? sleep_us : 1000;
sleep_milliseconds = sleep_microseconds / 1000; sleep_milliseconds = sleep_microseconds / 1000;
max_attempts_global = max_attempts > 0 ? max_attempts : 5; max_attempts_global = max_attempts > 0 ? max_attempts : 5;
max_backoff_us_global = max_backoff > 0 ? max_backoff : 500000; max_backoff_us_global = max_backoff > 0 ? max_backoff : 500000;
// Set buffer configuration
buffer_period_count = (buf_periods >= 2 && buf_periods <= 24) ? buf_periods : 12; buffer_period_count = (buf_periods >= 2 && buf_periods <= 24) ? buf_periods : 12;
// Note: sr and fs parameters ignored - always 48kHz with 960 samples
} }
/** /**
@ -140,19 +161,19 @@ void update_audio_decoder_constants(uint32_t sr, uint8_t ch, uint16_t fs, uint16
* Must be called before jetkvm_audio_capture_init or jetkvm_audio_playback_init * Must be called before jetkvm_audio_capture_init or jetkvm_audio_playback_init
* *
* Device mapping (set via ALSA_CAPTURE_DEVICE/ALSA_PLAYBACK_DEVICE): * Device mapping (set via ALSA_CAPTURE_DEVICE/ALSA_PLAYBACK_DEVICE):
* plughw:0,0 = TC358743 HDMI audio with rate conversion (for OUTPUT path capture) * hw:0,0 = TC358743 HDMI audio (direct hardware access, SpeexDSP resampling)
* plughw:1,0 = USB Audio Gadget with rate conversion (for OUTPUT path capture or INPUT path playback) * hw:1,0 = USB Audio Gadget (direct hardware access, SpeexDSP resampling)
*/ */
static void init_alsa_devices_from_env(void) { static void init_alsa_devices_from_env(void) {
// Always read from environment to support device switching // Always read from environment to support device switching
alsa_capture_device = getenv("ALSA_CAPTURE_DEVICE"); alsa_capture_device = getenv("ALSA_CAPTURE_DEVICE");
if (alsa_capture_device == NULL || alsa_capture_device[0] == '\0') { if (alsa_capture_device == NULL || alsa_capture_device[0] == '\0') {
alsa_capture_device = "plughw:1,0"; // Default: USB gadget audio for capture with rate conversion alsa_capture_device = "hw:1,0"; // Default: USB gadget audio for capture
} }
alsa_playback_device = getenv("ALSA_PLAYBACK_DEVICE"); alsa_playback_device = getenv("ALSA_PLAYBACK_DEVICE");
if (alsa_playback_device == NULL || alsa_playback_device[0] == '\0') { if (alsa_playback_device == NULL || alsa_playback_device[0] == '\0') {
alsa_playback_device = "plughw:1,0"; // Default: USB gadget audio for playback with rate conversion alsa_playback_device = "hw:1,0"; // Default: USB gadget audio for playback
} }
} }
@ -197,7 +218,7 @@ static volatile sig_atomic_t playback_initialized = 0;
* Open ALSA device with exponential backoff retry * Open ALSA device with exponential backoff retry
* @return 0 on success, negative error code on failure * @return 0 on success, negative error code on failure
*/ */
// Helper: High-precision sleep using nanosleep (better than usleep) // High-precision sleep using nanosleep
static inline void precise_sleep_us(uint32_t microseconds) { static inline void precise_sleep_us(uint32_t microseconds) {
struct timespec ts = { struct timespec ts = {
.tv_sec = microseconds / 1000000, .tv_sec = microseconds / 1000000,
@ -220,12 +241,12 @@ static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream
attempt++; attempt++;
// Apply different sleep strategies based on error type // Apply sleep strategy based on error type
if (err == -EPERM || err == -EACCES) { if (err == -EPERM || err == -EACCES) {
precise_sleep_us(backoff_us >> 1); // Shorter wait for permission errors precise_sleep_us(backoff_us >> 1); // Shorter wait for permission errors
} else { } else {
precise_sleep_us(backoff_us); precise_sleep_us(backoff_us);
// Exponential backoff for all retry-worthy errors // Exponential backoff for retry-worthy errors
if (err == -EBUSY || err == -EAGAIN || err == -ENODEV || err == -ENOENT) { if (err == -EBUSY || err == -EAGAIN || err == -ENODEV || err == -ENOENT) {
backoff_us = (backoff_us < 50000) ? (backoff_us << 1) : 50000; backoff_us = (backoff_us < 50000) ? (backoff_us << 1) : 50000;
} }
@ -345,12 +366,12 @@ static int handle_alsa_error(snd_pcm_t *handle, snd_pcm_t **valid_handle,
} }
/** /**
* Configure ALSA device (S16_LE @ variable rate with optimized buffering) * Configure ALSA device (S16_LE @ hardware-negotiated rate with optimized buffering)
* @param handle ALSA PCM handle * @param handle ALSA PCM handle
* @param device_name Device name for logging * @param device_name Device name for logging
* @param num_channels Number of channels (1=mono, 2=stereo) * @param num_channels Number of channels (1=mono, 2=stereo)
* @param actual_rate_out Pointer to store the actual rate the device was configured to use * @param actual_rate_out Pointer to store the actual hardware-negotiated rate
* @param actual_frame_size_out Pointer to store the actual frame size (samples per channel) * @param actual_frame_size_out Pointer to store the actual frame size at hardware rate
* @return 0 on success, negative error code on failure * @return 0 on success, negative error code on failure
*/ */
static int configure_alsa_device(snd_pcm_t *handle, const char *device_name, uint8_t num_channels, static int configure_alsa_device(snd_pcm_t *handle, const char *device_name, uint8_t num_channels,
@ -368,23 +389,43 @@ static int configure_alsa_device(snd_pcm_t *handle, const char *device_name, uin
if (err < 0) return err; if (err < 0) return err;
err = snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED); err = snd_pcm_hw_params_set_access(handle, params, SND_PCM_ACCESS_RW_INTERLEAVED);
if (err < 0) return err; if (err < 0) {
fprintf(stderr, "ERROR: %s: Failed to set access mode: %s\n", device_name, snd_strerror(err));
fflush(stderr);
return err;
}
err = snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE); err = snd_pcm_hw_params_set_format(handle, params, SND_PCM_FORMAT_S16_LE);
if (err < 0) return err; if (err < 0) {
fprintf(stderr, "ERROR: %s: Failed to set format S16_LE: %s\n", device_name, snd_strerror(err));
fflush(stderr);
return err;
}
err = snd_pcm_hw_params_set_channels(handle, params, num_channels); err = snd_pcm_hw_params_set_channels(handle, params, num_channels);
if (err < 0) {
fprintf(stderr, "ERROR: %s: Failed to set %u channels: %s\n", device_name, num_channels, snd_strerror(err));
fflush(stderr);
return err;
}
// Disable ALSA resampling - we handle it with SpeexDSP
err = snd_pcm_hw_params_set_rate_resample(handle, params, 0);
if (err < 0) {
fprintf(stderr, "ERROR: %s: Failed to disable ALSA resampling: %s\n", device_name, snd_strerror(err));
fflush(stderr);
return err;
}
// Try to set 48kHz first (preferred), then let hardware negotiate
unsigned int requested_rate = opus_sample_rate;
err = snd_pcm_hw_params_set_rate_near(handle, params, &requested_rate, 0);
if (err < 0) return err; if (err < 0) return err;
err = snd_pcm_hw_params_set_rate_resample(handle, params, 1); // Calculate frame size for this hardware rate (20ms)
if (err < 0) return err; uint16_t hw_frame_size = requested_rate / 50;
err = snd_pcm_hw_params_set_rate(handle, params, sample_rate, 0); snd_pcm_uframes_t period_size = hw_frame_size;
if (err < 0) return err;
uint16_t actual_frame_size = frame_size;
snd_pcm_uframes_t period_size = actual_frame_size;
if (period_size < 64) period_size = 64; if (period_size < 64) period_size = 64;
err = snd_pcm_hw_params_set_period_size_near(handle, params, &period_size, 0); err = snd_pcm_hw_params_set_period_size_near(handle, params, &period_size, 0);
@ -399,12 +440,17 @@ static int configure_alsa_device(snd_pcm_t *handle, const char *device_name, uin
unsigned int verified_rate = 0; unsigned int verified_rate = 0;
err = snd_pcm_hw_params_get_rate(params, &verified_rate, 0); err = snd_pcm_hw_params_get_rate(params, &verified_rate, 0);
if (err < 0 || verified_rate != sample_rate) { if (err < 0) {
fprintf(stderr, "WARNING: %s: Rate verification failed - expected %u Hz, got %u Hz\n", fprintf(stderr, "ERROR: %s: Failed to get rate: %s\n",
device_name, sample_rate, verified_rate); device_name, snd_strerror(err));
fflush(stderr); fflush(stderr);
return err;
} }
fprintf(stderr, "INFO: %s: Hardware negotiated %u Hz (Opus uses %u Hz with SpeexDSP resampling)\n",
device_name, verified_rate, opus_sample_rate);
fflush(stderr);
err = snd_pcm_sw_params_current(handle, sw_params); err = snd_pcm_sw_params_current(handle, sw_params);
if (err < 0) return err; if (err < 0) return err;
@ -420,8 +466,8 @@ static int configure_alsa_device(snd_pcm_t *handle, const char *device_name, uin
err = snd_pcm_prepare(handle); err = snd_pcm_prepare(handle);
if (err < 0) return err; if (err < 0) return err;
if (actual_rate_out) *actual_rate_out = sample_rate; if (actual_rate_out) *actual_rate_out = verified_rate;
if (actual_frame_size_out) *actual_frame_size_out = actual_frame_size; if (actual_frame_size_out) *actual_frame_size_out = hw_frame_size;
return 0; return 0;
} }
@ -430,9 +476,9 @@ static int configure_alsa_device(snd_pcm_t *handle, const char *device_name, uin
/** /**
* Initialize OUTPUT path (HDMI or USB Gadget audio capture Opus encoder) * Initialize OUTPUT path (HDMI or USB Gadget audio capture Opus encoder)
* Opens ALSA capture device from ALSA_CAPTURE_DEVICE env (default: plughw:1,0, set to plughw:0,0 for HDMI) * Opens ALSA capture device from ALSA_CAPTURE_DEVICE env (default: hw:1,0, set to hw:0,0 for HDMI)
* and creates Opus encoder with optimized settings * and creates Opus encoder with optimized settings
* @return 0 on success, -EBUSY if initializing, -1/-2/-3 on errors * @return 0 on success, -EBUSY if initializing, -1/-2/-3/-4 on errors
*/ */
int jetkvm_audio_capture_init() { int jetkvm_audio_capture_init() {
int err; int err;
@ -492,13 +538,65 @@ int jetkvm_audio_capture_init() {
return -2; return -2;
} }
// Store hardware-negotiated values
hardware_sample_rate = actual_rate;
hardware_frame_size = actual_frame_size;
// Validate hardware frame size
if (hardware_frame_size > 3840) {
fprintf(stderr, "ERROR: capture: Hardware frame size %u exceeds buffer capacity 3840\n",
hardware_frame_size);
fflush(stderr);
snd_pcm_t *handle = pcm_capture_handle;
pcm_capture_handle = NULL;
snd_pcm_close(handle);
atomic_store(&capture_stop_requested, 0);
capture_initializing = 0;
return -4;
}
// Clean up any existing resampler before creating new one (prevents memory leak on re-init)
if (capture_resampler) {
speex_resampler_destroy(capture_resampler);
capture_resampler = NULL;
}
// Initialize Speex resampler if hardware rate != 48kHz
if (hardware_sample_rate != opus_sample_rate) {
int speex_err = 0;
capture_resampler = speex_resampler_init(capture_channels, hardware_sample_rate,
opus_sample_rate, SPEEX_RESAMPLER_QUALITY_DESKTOP,
&speex_err);
if (!capture_resampler || speex_err != 0) {
fprintf(stderr, "ERROR: capture: Failed to create SpeexDSP resampler (%u Hz → %u Hz): %d\n",
hardware_sample_rate, opus_sample_rate, speex_err);
fflush(stderr);
snd_pcm_t *handle = pcm_capture_handle;
pcm_capture_handle = NULL;
snd_pcm_close(handle);
atomic_store(&capture_stop_requested, 0);
capture_initializing = 0;
return -3;
}
fprintf(stderr, "INFO: capture: SpeexDSP resampler initialized (%u Hz → %u Hz)\n",
hardware_sample_rate, opus_sample_rate);
fflush(stderr);
} else {
fprintf(stderr, "INFO: capture: No resampling needed (hardware = Opus = %u Hz)\n", opus_sample_rate);
fflush(stderr);
}
fprintf(stderr, "INFO: capture: Initializing Opus encoder at %u Hz, %u channels, frame size %u\n", fprintf(stderr, "INFO: capture: Initializing Opus encoder at %u Hz, %u channels, frame size %u\n",
actual_rate, capture_channels, actual_frame_size); opus_sample_rate, capture_channels, opus_frame_size);
fflush(stderr); fflush(stderr);
int opus_err = 0; int opus_err = 0;
encoder = opus_encoder_create(actual_rate, capture_channels, OPUS_APPLICATION_AUDIO, &opus_err); encoder = opus_encoder_create(opus_sample_rate, capture_channels, OPUS_APPLICATION_AUDIO, &opus_err);
if (!encoder || opus_err != OPUS_OK) { if (!encoder || opus_err != OPUS_OK) {
if (capture_resampler) {
speex_resampler_destroy(capture_resampler);
capture_resampler = NULL;
}
if (pcm_capture_handle) { if (pcm_capture_handle) {
snd_pcm_t *handle = pcm_capture_handle; snd_pcm_t *handle = pcm_capture_handle;
pcm_capture_handle = NULL; pcm_capture_handle = NULL;
@ -506,20 +604,29 @@ int jetkvm_audio_capture_init() {
} }
atomic_store(&capture_stop_requested, 0); atomic_store(&capture_stop_requested, 0);
capture_initializing = 0; capture_initializing = 0;
return -3; return -4;
} }
opus_encoder_ctl(encoder, OPUS_SET_BITRATE(opus_bitrate)); #define OPUS_CTL_WARN(call, desc) do { \
opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(opus_complexity)); int _err = call; \
opus_encoder_ctl(encoder, OPUS_SET_VBR(OPUS_VBR)); if (_err != OPUS_OK) { \
opus_encoder_ctl(encoder, OPUS_SET_VBR_CONSTRAINT(OPUS_VBR_CONSTRAINT)); fprintf(stderr, "WARN: capture: Failed to set " desc ": %s\n", opus_strerror(_err)); \
opus_encoder_ctl(encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_TYPE)); fflush(stderr); \
opus_encoder_ctl(encoder, OPUS_SET_BANDWIDTH(OPUS_BANDWIDTH)); } \
opus_encoder_ctl(encoder, OPUS_SET_DTX(opus_dtx_enabled)); } while(0)
opus_encoder_ctl(encoder, OPUS_SET_LSB_DEPTH(OPUS_LSB_DEPTH));
opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(opus_fec_enabled)); OPUS_CTL_WARN(opus_encoder_ctl(encoder, OPUS_SET_BITRATE(opus_bitrate)), "bitrate");
opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(opus_packet_loss_perc)); OPUS_CTL_WARN(opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(opus_complexity)), "complexity");
OPUS_CTL_WARN(opus_encoder_ctl(encoder, OPUS_SET_VBR(OPUS_VBR)), "VBR mode");
OPUS_CTL_WARN(opus_encoder_ctl(encoder, OPUS_SET_VBR_CONSTRAINT(OPUS_VBR_CONSTRAINT)), "VBR constraint");
OPUS_CTL_WARN(opus_encoder_ctl(encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_TYPE)), "signal type");
OPUS_CTL_WARN(opus_encoder_ctl(encoder, OPUS_SET_BANDWIDTH(OPUS_BANDWIDTH)), "bandwidth");
OPUS_CTL_WARN(opus_encoder_ctl(encoder, OPUS_SET_DTX(opus_dtx_enabled)), "DTX");
OPUS_CTL_WARN(opus_encoder_ctl(encoder, OPUS_SET_LSB_DEPTH(OPUS_LSB_DEPTH)), "LSB depth");
OPUS_CTL_WARN(opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(opus_fec_enabled)), "FEC");
OPUS_CTL_WARN(opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(opus_packet_loss_perc)), "packet loss percentage");
#undef OPUS_CTL_WARN
capture_initialized = 1; capture_initialized = 1;
atomic_store(&capture_stop_requested, 0); atomic_store(&capture_stop_requested, 0);
@ -528,12 +635,14 @@ int jetkvm_audio_capture_init() {
} }
/** /**
* Read HDMI audio, encode to Opus (OUTPUT path hot function) * Read HDMI audio, resample with SpeexDSP, encode to Opus (OUTPUT path hot function)
* @param opus_buf Output buffer for encoded Opus packet * @param opus_buf Output buffer for encoded Opus packet
* @return >0 = Opus packet size in bytes, -1 = error * @return >0 = Opus packet size in bytes, -1 = error
*/ */
__attribute__((hot)) int jetkvm_audio_read_encode(void * __restrict__ opus_buf) { __attribute__((hot)) int jetkvm_audio_read_encode(void * __restrict__ opus_buf) {
static short CACHE_ALIGN pcm_buffer[960 * 2]; // Cache-aligned // Two buffers: hardware buffer + resampled buffer (at 48kHz)
static short CACHE_ALIGN pcm_hw_buffer[3840 * 2]; // Max 192kHz @ 20ms * 2 channels
static short CACHE_ALIGN pcm_opus_buffer[960 * 2]; // 48kHz @ 20ms * 2 channels
unsigned char * __restrict__ out = (unsigned char*)opus_buf; unsigned char * __restrict__ out = (unsigned char*)opus_buf;
int32_t pcm_rc, nb_bytes; int32_t pcm_rc, nb_bytes;
int32_t err = 0; int32_t err = 0;
@ -545,8 +654,8 @@ __attribute__((hot)) int jetkvm_audio_read_encode(void * __restrict__ opus_buf)
} }
SIMD_PREFETCH(out, 1, 0); SIMD_PREFETCH(out, 1, 0);
SIMD_PREFETCH(pcm_buffer, 0, 0); SIMD_PREFETCH(pcm_hw_buffer, 0, 0);
SIMD_PREFETCH(pcm_buffer + 64, 0, 1); SIMD_PREFETCH(pcm_hw_buffer + 64, 0, 1);
// Acquire mutex to protect against concurrent close // Acquire mutex to protect against concurrent close
pthread_mutex_lock(&capture_mutex); pthread_mutex_lock(&capture_mutex);
@ -564,7 +673,8 @@ retry_read:
snd_pcm_t *handle = pcm_capture_handle; snd_pcm_t *handle = pcm_capture_handle;
pcm_rc = snd_pcm_readi(handle, pcm_buffer, frame_size); // Read from hardware at hardware sample rate
pcm_rc = snd_pcm_readi(handle, pcm_hw_buffer, hardware_frame_size);
if (handle != pcm_capture_handle) { if (handle != pcm_capture_handle) {
pthread_mutex_unlock(&capture_mutex); pthread_mutex_unlock(&capture_mutex);
@ -585,9 +695,29 @@ retry_read:
} }
// Zero-pad if we got a short read // Zero-pad if we got a short read
if (__builtin_expect(pcm_rc < frame_size, 0)) { if (__builtin_expect(pcm_rc < hardware_frame_size, 0)) {
uint32_t remaining_samples = (frame_size - pcm_rc) * capture_channels; uint32_t remaining_samples = (hardware_frame_size - pcm_rc) * capture_channels;
simd_clear_samples_s16(&pcm_buffer[pcm_rc * capture_channels], remaining_samples); simd_clear_samples_s16(&pcm_hw_buffer[pcm_rc * capture_channels], remaining_samples);
}
// Resample to 48kHz if needed
short *pcm_to_encode;
if (capture_resampler) {
spx_uint32_t in_len = hardware_frame_size;
spx_uint32_t out_len = opus_frame_size;
int res_err = speex_resampler_process_interleaved_int(capture_resampler,
pcm_hw_buffer, &in_len,
pcm_opus_buffer, &out_len);
if (res_err != 0 || out_len != opus_frame_size) {
fprintf(stderr, "ERROR: capture: Resampling failed (err=%d, out_len=%u, expected=%u)\n",
res_err, out_len, opus_frame_size);
fflush(stderr);
pthread_mutex_unlock(&capture_mutex);
return -1;
}
pcm_to_encode = pcm_opus_buffer;
} else {
pcm_to_encode = pcm_hw_buffer;
} }
OpusEncoder *enc = encoder; OpusEncoder *enc = encoder;
@ -596,7 +726,12 @@ retry_read:
return -1; return -1;
} }
nb_bytes = opus_encode(enc, pcm_buffer, frame_size, out, max_packet_size); nb_bytes = opus_encode(enc, pcm_to_encode, opus_frame_size, out, max_packet_size);
if (__builtin_expect(nb_bytes < 0, 0)) {
fprintf(stderr, "ERROR: capture: Opus encoding failed: %s\n", opus_strerror(nb_bytes));
fflush(stderr);
}
pthread_mutex_unlock(&capture_mutex); pthread_mutex_unlock(&capture_mutex);
return nb_bytes; return nb_bytes;
@ -606,7 +741,7 @@ retry_read:
/** /**
* Initialize INPUT path (Opus decoder device speakers) * Initialize INPUT path (Opus decoder device speakers)
* Opens ALSA playback device from ALSA_PLAYBACK_DEVICE env (default: plughw:1,0) * Opens ALSA playback device from ALSA_PLAYBACK_DEVICE env (default: hw:1,0)
* and creates Opus decoder. Returns immediately on device open failure (no fallback). * and creates Opus decoder. Returns immediately on device open failure (no fallback).
* @return 0 on success, -EBUSY if initializing, -1/-2 on errors * @return 0 on success, -EBUSY if initializing, -1/-2 on errors
*/ */
@ -731,10 +866,10 @@ __attribute__((hot)) int jetkvm_audio_decode_write(void * __restrict__ opus_buf,
// Decode Opus packet to PCM (FEC automatically applied if embedded in packet) // 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) // decode_fec=0 means normal decode (FEC data is used automatically when present)
pcm_frames = opus_decode(dec, in, opus_size, pcm_buffer, frame_size, 0); pcm_frames = opus_decode(dec, in, opus_size, pcm_buffer, opus_frame_size, 0);
if (__builtin_expect(pcm_frames < 0, 0)) { if (__builtin_expect(pcm_frames < 0, 0)) {
pcm_frames = opus_decode(dec, NULL, 0, pcm_buffer, frame_size, 1); pcm_frames = opus_decode(dec, NULL, 0, pcm_buffer, opus_frame_size, 1);
if (pcm_frames < 0) { if (pcm_frames < 0) {
pthread_mutex_unlock(&playback_mutex); pthread_mutex_unlock(&playback_mutex);
@ -810,6 +945,13 @@ static void close_audio_stream(atomic_int *stop_requested, volatile int *initial
*pcm_handle = NULL; *pcm_handle = NULL;
*codec = NULL; *codec = NULL;
// Clean up resampler inside mutex to prevent race with encoding thread
if (mutex == &capture_mutex && capture_resampler) {
SpeexResamplerState *res = capture_resampler;
capture_resampler = NULL;
speex_resampler_destroy(res);
}
pthread_mutex_unlock(mutex); pthread_mutex_unlock(mutex);
if (handle_to_close) { if (handle_to_close) {

View File

@ -3,8 +3,8 @@
package audio package audio
/* /*
#cgo CFLAGS: -O3 -ffast-math -I/opt/jetkvm-audio-libs/alsa-lib-1.2.14/include -I/opt/jetkvm-audio-libs/opus-1.5.2/include #cgo CFLAGS: -O3 -ffast-math -I/opt/jetkvm-audio-libs/alsa-lib-1.2.14/include -I/opt/jetkvm-audio-libs/opus-1.5.2/include -I/opt/jetkvm-audio-libs/speexdsp-1.2.1/include
#cgo LDFLAGS: /opt/jetkvm-audio-libs/alsa-lib-1.2.14/src/.libs/libasound.a /opt/jetkvm-audio-libs/opus-1.5.2/.libs/libopus.a -lm -ldl -lpthread #cgo LDFLAGS: /opt/jetkvm-audio-libs/alsa-lib-1.2.14/src/.libs/libasound.a /opt/jetkvm-audio-libs/opus-1.5.2/.libs/libopus.a /opt/jetkvm-audio-libs/speexdsp-1.2.1/libspeexdsp/.libs/libspeexdsp.a -lm -ldl -lpthread
#include <stdlib.h> #include <stdlib.h>
#include "c/audio.c" #include "c/audio.c"
@ -83,16 +83,10 @@ func (c *CgoSource) Connect() error {
func (c *CgoSource) connectOutput() error { func (c *CgoSource) connectOutput() error {
os.Setenv("ALSA_CAPTURE_DEVICE", c.alsaDevice) os.Setenv("ALSA_CAPTURE_DEVICE", c.alsaDevice)
// Using plughw: enables ALSA rate conversion plugin // Opus uses fixed 48kHz sample rate (RFC 7587)
// USB Gadget hardware is fixed at 48kHz (configfs hardcoded), so keep it at 48kHz // SpeexDSP handles any hardware rate conversion
// HDMI can use configured rate - plughw resamples from hardware rate to Opus rate const sampleRate = 48000
sampleRate := c.config.SampleRate const frameSize = 960 // 20ms at 48kHz
if c.alsaDevice == "plughw:1,0" {
sampleRate = 48000
} else if sampleRate == 0 {
sampleRate = 48000
}
frameSize := uint16(sampleRate * 20 / 1000)
c.logger.Debug(). c.logger.Debug().
Uint16("bitrate_kbps", c.config.Bitrate). Uint16("bitrate_kbps", c.config.Bitrate).
@ -101,7 +95,7 @@ func (c *CgoSource) connectOutput() error {
Bool("fec", c.config.FECEnabled). Bool("fec", c.config.FECEnabled).
Uint8("buffer_periods", c.config.BufferPeriods). Uint8("buffer_periods", c.config.BufferPeriods).
Uint32("sample_rate", sampleRate). Uint32("sample_rate", sampleRate).
Uint16("frame_size", frameSize). Uint16("frame_size", uint16(frameSize)).
Uint8("packet_loss_perc", c.config.PacketLossPerc). Uint8("packet_loss_perc", c.config.PacketLossPerc).
Msg("Initializing audio capture") Msg("Initializing audio capture")
@ -134,15 +128,14 @@ func (c *CgoSource) connectOutput() error {
func (c *CgoSource) connectInput() error { func (c *CgoSource) connectInput() error {
os.Setenv("ALSA_PLAYBACK_DEVICE", c.alsaDevice) os.Setenv("ALSA_PLAYBACK_DEVICE", c.alsaDevice)
// USB Audio Gadget (hw:1,0) is hardcoded to 48kHz in usbgadget/config.go // USB Audio Gadget uses fixed 48kHz sample rate
// Always use 48kHz for input path regardless of UI configuration
const inputSampleRate = 48000 const inputSampleRate = 48000
frameSize := uint16(inputSampleRate * 20 / 1000) const frameSize = 960 // 20ms at 48kHz
C.update_audio_decoder_constants( C.update_audio_decoder_constants(
C.uint(inputSampleRate), C.uint(inputSampleRate),
C.uchar(1), C.uchar(1), // Mono for USB audio gadget
C.ushort(frameSize), C.ushort(uint16(frameSize)),
C.ushort(1500), C.ushort(1500),
C.uint(1000), C.uint(1000),
C.uchar(5), C.uchar(5),

View File

@ -10,7 +10,6 @@ type AudioConfig struct {
BufferPeriods uint8 BufferPeriods uint8
DTXEnabled bool DTXEnabled bool
FECEnabled bool FECEnabled bool
SampleRate uint32
PacketLossPerc uint8 PacketLossPerc uint8
} }
@ -21,7 +20,6 @@ func DefaultAudioConfig() AudioConfig {
BufferPeriods: 12, BufferPeriods: 12,
DTXEnabled: true, DTXEnabled: true,
FECEnabled: true, FECEnabled: true,
SampleRate: 48000,
PacketLossPerc: 0, PacketLossPerc: 0,
} }
} }

View File

@ -982,7 +982,6 @@ type AudioConfigResponse struct {
DTXEnabled bool `json:"dtx_enabled"` DTXEnabled bool `json:"dtx_enabled"`
FECEnabled bool `json:"fec_enabled"` FECEnabled bool `json:"fec_enabled"`
BufferPeriods int `json:"buffer_periods"` BufferPeriods int `json:"buffer_periods"`
SampleRate int `json:"sample_rate"`
PacketLossPerc int `json:"packet_loss_perc"` PacketLossPerc int `json:"packet_loss_perc"`
} }
@ -995,12 +994,11 @@ func rpcGetAudioConfig() (AudioConfigResponse, error) {
DTXEnabled: cfg.DTXEnabled, DTXEnabled: cfg.DTXEnabled,
FECEnabled: cfg.FECEnabled, FECEnabled: cfg.FECEnabled,
BufferPeriods: int(cfg.BufferPeriods), BufferPeriods: int(cfg.BufferPeriods),
SampleRate: int(cfg.SampleRate),
PacketLossPerc: int(cfg.PacketLossPerc), PacketLossPerc: int(cfg.PacketLossPerc),
}, nil }, nil
} }
func rpcSetAudioConfig(bitrate int, complexity int, dtxEnabled bool, fecEnabled bool, bufferPeriods int, sampleRate int, packetLossPerc int) error { func rpcSetAudioConfig(bitrate int, complexity int, dtxEnabled bool, fecEnabled bool, bufferPeriods int, packetLossPerc int) error {
ensureConfigLoaded() ensureConfigLoaded()
if bitrate < 64 || bitrate > 256 { if bitrate < 64 || bitrate > 256 {
@ -1012,10 +1010,6 @@ func rpcSetAudioConfig(bitrate int, complexity int, dtxEnabled bool, fecEnabled
if bufferPeriods < 2 || bufferPeriods > 24 { if bufferPeriods < 2 || bufferPeriods > 24 {
return fmt.Errorf("buffer periods must be between 2 and 24") return fmt.Errorf("buffer periods must be between 2 and 24")
} }
validSampleRates := map[int]bool{8000: true, 12000: true, 16000: true, 24000: true, 48000: true}
if !validSampleRates[sampleRate] {
return fmt.Errorf("sample rate must be one of: 8000, 12000, 16000, 24000, 48000 Hz")
}
if packetLossPerc < 0 || packetLossPerc > 100 { if packetLossPerc < 0 || packetLossPerc > 100 {
return fmt.Errorf("packet loss percentage must be between 0 and 100") return fmt.Errorf("packet loss percentage must be between 0 and 100")
} }
@ -1025,7 +1019,6 @@ func rpcSetAudioConfig(bitrate int, complexity int, dtxEnabled bool, fecEnabled
config.AudioDTXEnabled = dtxEnabled config.AudioDTXEnabled = dtxEnabled
config.AudioFECEnabled = fecEnabled config.AudioFECEnabled = fecEnabled
config.AudioBufferPeriods = bufferPeriods config.AudioBufferPeriods = bufferPeriods
config.AudioSampleRate = sampleRate
config.AudioPacketLossPerc = packetLossPerc config.AudioPacketLossPerc = packetLossPerc
return SaveConfig() return SaveConfig()
@ -1380,7 +1373,7 @@ var rpcHandlers = map[string]RPCHandler{
"setAudioOutputSource": {Func: rpcSetAudioOutputSource, Params: []string{"source"}}, "setAudioOutputSource": {Func: rpcSetAudioOutputSource, Params: []string{"source"}},
"refreshHdmiConnection": {Func: rpcRefreshHdmiConnection}, "refreshHdmiConnection": {Func: rpcRefreshHdmiConnection},
"getAudioConfig": {Func: rpcGetAudioConfig}, "getAudioConfig": {Func: rpcGetAudioConfig},
"setAudioConfig": {Func: rpcSetAudioConfig, Params: []string{"bitrate", "complexity", "dtxEnabled", "fecEnabled", "bufferPeriods", "sampleRate", "packetLossPerc"}}, "setAudioConfig": {Func: rpcSetAudioConfig, Params: []string{"bitrate", "complexity", "dtxEnabled", "fecEnabled", "bufferPeriods", "packetLossPerc"}},
"restartAudioOutput": {Func: rpcRestartAudioOutput}, "restartAudioOutput": {Func: rpcRestartAudioOutput},
"getAudioInputAutoEnable": {Func: rpcGetAudioInputAutoEnable}, "getAudioInputAutoEnable": {Func: rpcGetAudioInputAutoEnable},
"setAudioInputAutoEnable": {Func: rpcSetAudioInputAutoEnable, Params: []string{"enabled"}}, "setAudioInputAutoEnable": {Func: rpcSetAudioInputAutoEnable, Params: []string{"enabled"}},

View File

@ -260,18 +260,20 @@ fi
if [ "$INSTALL_APP" = true ] if [ "$INSTALL_APP" = true ]
then then
msg_info "▶ Building release binary" msg_info "▶ Building release binary"
# Build audio dependencies and release binary
do_make build_audio_deps
do_make build_release \ do_make build_release \
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \ SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \ SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE} ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
# Copy the binary to the remote host as if we were the OTA updater. # Deploy as OTA update and reboot
sshdev "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app sshdev "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
# Reboot the device, the new app will be deployed by the startup process.
sshdev "reboot" sshdev "reboot"
else else
msg_info "▶ Building development binary" msg_info "▶ Building development binary"
# Build audio dependencies and development binary
do_make build_audio_deps
do_make build_dev \ do_make build_dev \
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \ SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \ SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \

View File

@ -16,7 +16,6 @@ interface AudioConfigResult {
dtx_enabled: boolean; dtx_enabled: boolean;
fec_enabled: boolean; fec_enabled: boolean;
buffer_periods: number; buffer_periods: number;
sample_rate: number;
packet_loss_perc: number; packet_loss_perc: number;
} }
@ -54,8 +53,6 @@ export default function SettingsAudioRoute() {
setAudioFECEnabled, setAudioFECEnabled,
audioBufferPeriods, audioBufferPeriods,
setAudioBufferPeriods, setAudioBufferPeriods,
audioSampleRate,
setAudioSampleRate,
audioPacketLossPerc, audioPacketLossPerc,
setAudioPacketLossPerc, setAudioPacketLossPerc,
} = useSettingsStore(); } = useSettingsStore();
@ -84,10 +81,9 @@ export default function SettingsAudioRoute() {
setAudioDTXEnabled(config.dtx_enabled); setAudioDTXEnabled(config.dtx_enabled);
setAudioFECEnabled(config.fec_enabled); setAudioFECEnabled(config.fec_enabled);
setAudioBufferPeriods(config.buffer_periods); setAudioBufferPeriods(config.buffer_periods);
setAudioSampleRate(config.sample_rate);
setAudioPacketLossPerc(config.packet_loss_perc); setAudioPacketLossPerc(config.packet_loss_perc);
}); });
}, [send, setAudioOutputEnabled, setAudioInputAutoEnable, setAudioOutputSource, setAudioBitrate, setAudioComplexity, setAudioDTXEnabled, setAudioFECEnabled, setAudioBufferPeriods, setAudioSampleRate, setAudioPacketLossPerc]); }, [send, setAudioOutputEnabled, setAudioInputAutoEnable, setAudioOutputSource, setAudioBitrate, setAudioComplexity, setAudioDTXEnabled, setAudioFECEnabled, setAudioBufferPeriods, setAudioPacketLossPerc]);
const handleAudioOutputEnabledChange = (enabled: boolean) => { const handleAudioOutputEnabledChange = (enabled: boolean) => {
send("setAudioOutputEnabled", { enabled }, (resp: JsonRpcResponse) => { send("setAudioOutputEnabled", { enabled }, (resp: JsonRpcResponse) => {
@ -138,11 +134,10 @@ export default function SettingsAudioRoute() {
dtxEnabled: audioDTXEnabled, dtxEnabled: audioDTXEnabled,
fecEnabled: audioFECEnabled, fecEnabled: audioFECEnabled,
bufferPeriods: audioBufferPeriods, bufferPeriods: audioBufferPeriods,
sampleRate: audioSampleRate,
packetLossPerc: audioPacketLossPerc, packetLossPerc: audioPacketLossPerc,
}); });
const handleAudioConfigChange = (updates: Partial<typeof getCurrentConfig>) => { const handleAudioConfigChange = (updates: Partial<ReturnType<typeof getCurrentConfig>>) => {
const config = { ...getCurrentConfig(), ...updates }; const config = { ...getCurrentConfig(), ...updates };
send("setAudioConfig", config, (resp: JsonRpcResponse) => { send("setAudioConfig", config, (resp: JsonRpcResponse) => {
@ -153,7 +148,6 @@ export default function SettingsAudioRoute() {
setAudioDTXEnabled(config.dtxEnabled); setAudioDTXEnabled(config.dtxEnabled);
setAudioFECEnabled(config.fecEnabled); setAudioFECEnabled(config.fecEnabled);
setAudioBufferPeriods(config.bufferPeriods); setAudioBufferPeriods(config.bufferPeriods);
setAudioSampleRate(config.sampleRate);
setAudioPacketLossPerc(config.packetLossPerc); setAudioPacketLossPerc(config.packetLossPerc);
notifications.success(m.audio_settings_config_updated()); notifications.success(m.audio_settings_config_updated());
}); });
@ -283,24 +277,6 @@ export default function SettingsAudioRoute() {
/> />
</SettingsItem> </SettingsItem>
<SettingsItem
title={m.audio_settings_sample_rate_title()}
description={m.audio_settings_sample_rate_description()}
>
<SelectMenuBasic
size="SM"
value={String(audioSampleRate)}
options={[
{ value: "8000", label: "8 kHz" },
{ value: "12000", label: "12 kHz" },
{ value: "16000", label: "16 kHz" },
{ value: "24000", label: "24 kHz" },
{ value: "48000", label: "48 kHz (default)" },
]}
onChange={(e) => handleAudioConfigChange({ sampleRate: parseInt(e.target.value) })}
/>
</SettingsItem>
<SettingsItem <SettingsItem
title={m.audio_settings_packet_loss_title()} title={m.audio_settings_packet_loss_title()}
description={m.audio_settings_packet_loss_description()} description={m.audio_settings_packet_loss_description()}