- Change logger.Info() to logger.Log() for shutdown message to ensure it always logs
- Move stopAudio() before logging to stop audio first, then log shutdown
Addresses PR #718 review comment
When browser refreshes (Shift+R), USB state changes from "configured" to
"default" temporarily. Previously, HID files remained open with stale file
handles, causing "transport endpoint shutdown" errors.
Now proactively close HID files when USB transitions away from "configured"
state, preventing writes to invalid endpoints.
Fixes USB HID transport endpoint shutdown errors during browser refresh.
Removed obvious comments that don't add value:
- cgo_source.go: Removed redundant status check comments
- audio.go: Consolidated mutex pattern comments
Kept important comments that explain non-obvious patterns:
- Why mutex is released before C calls (deadlock prevention)
- Why operations happen outside mutex (avoid blocking on CGO)
- Why single critical section is used (race condition prevention)
Problem:
Previous fix reduced but didn't eliminate the hang when switching audio
sources. The C layer was still blocking on snd_pcm_readi()/snd_pcm_writei()
while holding the mutex, preventing cleanup from proceeding.
Solution:
Call snd_pcm_drop() BEFORE acquiring the mutex in close functions. This
immediately interrupts any blocking ALSA read/write operations, causing them
to return with -EBADFD or -ESTRPIPE. The sequence is now:
1. Set stop_requested flag
2. Call snd_pcm_drop() to interrupt blocking I/O (no mutex needed - thread-safe)
3. Acquire mutex for cleanup
4. Close handles and free resources
5. Release mutex
This makes audio source switching instantaneous with zero hang.
Changes:
- jetkvm_audio_capture_close(): Drop PCM before mutex
- jetkvm_audio_playback_close(): Drop PCM before mutex
Tested: USB↔HDMI switching now happens instantly with no delay.
Problem:
When switching audio sources (USB to HDMI or vice versa), the application
would hang indefinitely. This was caused by a deadlock between Go and C
layers:
1. Main thread calls SetAudioOutputSource() → stopOutputAudio()
2. stopOutputAudio() calls outputRelay.Stop() which waits for goroutine
3. Goroutine is blocked in ReadMessage() holding Go mutex
4. ReadMessage() calls blocking C function jetkvm_audio_read_encode()
5. C function is blocked reading from ALSA device
6. Disconnect() can't acquire Go mutex to clean up
7. Deadlock: Main thread waiting for goroutine, goroutine waiting for ALSA
Solution:
Release the Go mutex BEFORE calling blocking C functions in ReadMessage()
and WriteMessage(). The C layer has its own pthread mutex protection and
handles stop requests via atomic flags. This allows:
- Disconnect() to acquire the mutex immediately
- C layer to detect stop request and return quickly
- Goroutines to exit cleanly
- Audio source switching to work flawlessly
Fixes:
- internal/audio/cgo_source.go:ReadMessage() - Release mutex before C call
- internal/audio/cgo_source.go:WriteMessage() - Release mutex before C call
This fix eliminates the hang when switching between USB and HDMI audio
sources.
Reduced mutex locking from 3 separate lock/unlock cycles to 1, eliminating
race window and improving performance. New relay is prepared within mutex,
then old resources are stopped and new relay started outside mutex to avoid
blocking during CGO calls.
Root cause: ALSA assertion failure `snd_pcm_writei: Assertion 'pcm' failed`
when pcm_playback_handle became NULL during concurrent write operations.
The crash occurred because:
1. Thread A checks pcm_playback_handle != NULL (passes)
2. Thread B calls jetkvm_audio_playback_close(), sets handle = NULL
3. Thread A calls snd_pcm_writei(NULL, ...) → SIGABRT
Solution: Added pthread mutexes to protect concurrent access:
- playback_mutex protects pcm_playback_handle in decode_write and close
- capture_mutex protects pcm_capture_handle in read_encode and close
All critical sections now acquire mutex before accessing ALSA handles,
preventing the NULL pointer from being passed to ALSA functions.
The sample rate cannot be configured by users - it's determined by the audio
source (HDMI device or USB gadget client). The previous UI gave the false
impression that users could select a sample rate, but the value was always
overridden by hardware detection.
Changes:
- Convert sample rate UI from dropdown to read-only display
- Show "(auto-detected from source)" label next to the value
- Remove sampleRate parameter from setAudioConfig RPC
- Update translations to clarify auto-detection
- Backend sample rate validation remains for backwards compatibility
The C code now automatically detects and adapts to whatever rate the hardware
supports, creating the Opus encoder/decoder with matching parameters to
eliminate pitch/speed distortion.
Root cause: ALSA was silently using a different sample rate than configured,
causing severe pitch/speed distortion (the "cassette player" warping effect).
The bug occurred when:
- User configured 48 kHz in UI
- HDMI source output 44.1 kHz audio
- set_rate() failed, set_rate_near() chose 44.1 kHz
- Code never checked what rate was actually set
- Opus encoder created for 48 kHz but received 44.1 kHz audio
- Result: ~9% pitch shift and timing mismatch
Fix:
- Always use set_rate_near() and check the actual rate returned
- Pass detected rate and frame size to Opus encoder/decoder creation
- Avoid modifying global state to prevent capture/playback interference
- Recalculate frame_size for 20ms at the actual rate
- Verify rate after hw_params application
- Add detailed logging for rate adaptation
This ensures Opus encoder/decoder use the correct rate matching the hardware,
regardless of what the HDMI source outputs.
- Update default EDID with registered manufacturer ID (Dell) and proper 24-inch display dimensions (52x32cm) for better macOS/OS compatibility
- Add configurable sample rate (32/44.1/48/96 kHz) to support different HDMI audio sources
- Add packet loss compensation percentage control for FEC overhead tuning
- Fix config migration to ensure new audio parameters get defaults for existing configs
- Update all language translations for new audio settings
When USB state transitions to "configured" (after reconnection or state
changes), automatically close and reopen HID file handles to ensure they
remain valid.
- Add Devices.Equals() method to compare USB device configurations
- Add GetGadgetDevices() to retrieve current device state
- Skip gadget reconfiguration when device state is unchanged
- Remove 6 unused audio translation keys from all language files
- Add config fields for bitrate, complexity, DTX, FEC, buffer periods
- Add RPC methods for get/set audio config and restart
- Add UI settings page with controls for all audio parameters
- Add Apply Settings button to restart audio with new config
- Add config migration for backwards compatibility
- Add translations for all 9 languages
- Clean up redundant comments and optimize log levels
Previous fix protected WriteMessage() but missed Connect() calls. During
session transfers, multiple goroutines could simultaneously initialize the
audio playback system, causing SIGABRT crashes in jetkvm_audio_playback_init.
Extended inputSourceMutex to serialize both Connect() and WriteMessage()
operations, ensuring atomic state transitions and preventing concurrent
C library initialization.
During session transfers, multiple handleInputTrackForSession goroutines
could call WriteMessage() concurrently on the shared inputSource, causing
Opus SILK decoder state corruption and assertion failures.
Added inputSourceMutex to serialize WriteMessage() calls and prevent
race conditions in both input and output audio paths.
When an old connection closed while a new one started, the audio cleanup
would hold audioMutex for up to 37 seconds during CGO disconnect calls,
blocking the new session from initializing.
Use capture-clear-release pattern to minimize mutex hold time:
- Capture references to sources/relays while holding mutex
- Clear globals immediately so new sessions can proceed
- Release mutex before calling blocking Stop/Disconnect operations
This eliminates the 37-second hang during connection transitions.