mirror of https://github.com/jetkvm/kvm.git
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:
parent
0be9dbcc6c
commit
9d86b02e66
|
|
@ -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"
|
||||||
|
|
|
||||||
44
audio.go
44
audio.go
|
|
@ -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)
|
||||||
}
|
}
|
||||||
if outputErr != nil {
|
return firstError(outputErr, inputErr)
|
||||||
return outputErr
|
}
|
||||||
|
|
||||||
|
func firstError(errs ...error) error {
|
||||||
|
for _, err := range errs {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
jsonrpc.go
11
jsonrpc.go
|
|
@ -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"}},
|
||||||
|
|
|
||||||
|
|
@ -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} \
|
||||||
|
|
|
||||||
|
|
@ -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()}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue