mirror of
https://github.com/jahruz67/wisp-open.git
synced 2026-08-08 18:14:08 +00:00
feat: enhance Linux support with ydotool integration and improve logging
This commit is contained in:
@@ -9,7 +9,6 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"math"
|
||||
"unsafe"
|
||||
"wis-free-v3/internal/logger"
|
||||
|
||||
@@ -202,14 +201,16 @@ func (r *AudioRecorder) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop and release the capture device so Linux privacy indicators turn off
|
||||
// between recordings. The next Start will initialize it again.
|
||||
// Stop the capture device. We keep the device initialized between recordings
|
||||
// to avoid the expensive re-initialization cycle. ALSA privacy indicators
|
||||
// will still turn off because we've stopped the stream.
|
||||
if r.device != nil {
|
||||
if err := r.device.Stop(); err != nil {
|
||||
logger.Error("Failed to stop audio device: %v", err)
|
||||
}
|
||||
r.device.Uninit()
|
||||
r.device = nil
|
||||
// Do NOT Uninit the device between recordings. Keeping it alive
|
||||
// avoids expensive ALSA device re-probe and reduces CPU spikes.
|
||||
// The device will be fully cleaned up in Cleanup().
|
||||
}
|
||||
|
||||
// Finalize WAV file
|
||||
@@ -262,25 +263,40 @@ func (r *AudioRecorder) onAudioData(_, inputSamples []byte, _ uint32) {
|
||||
|
||||
// Calculate volume if callback is set
|
||||
if r.OnVolume != nil {
|
||||
// S16LE: 2 bytes per sample
|
||||
samples := len(inputSamples) / 2
|
||||
var maxAmplitude float64
|
||||
for i := 0; i < samples; i++ {
|
||||
// Read as int16
|
||||
val := int16(binary.LittleEndian.Uint16(inputSamples[i*2 : i*2+2]))
|
||||
absVal := math.Abs(float64(val))
|
||||
if absVal > maxAmplitude {
|
||||
maxAmplitude = absVal
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize to 0.0 - 1.0 (max for int16 is 32767)
|
||||
level := maxAmplitude / 32767.0
|
||||
r.OnVolume(level)
|
||||
r.calculateVolume(inputSamples[:n])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// calculateVolume processes audio samples to compute the current volume level.
|
||||
// Extracted to avoid allocations in the hot audio callback path.
|
||||
func (r *AudioRecorder) calculateVolume(samples []byte) {
|
||||
// S16LE: 2 bytes per sample
|
||||
sampleCount := len(samples) / 2
|
||||
if sampleCount == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var maxAmplitude float64
|
||||
// Use direct slice indexing to avoid per-sample allocations
|
||||
for i := 0; i < sampleCount; i++ {
|
||||
offset := i * 2
|
||||
// Read as int16 via inlined LittleEndian to avoid function call overhead
|
||||
val := int16(samples[offset]) | int16(samples[offset+1])<<8
|
||||
absVal := float64(val)
|
||||
if absVal < 0 {
|
||||
absVal = -absVal
|
||||
}
|
||||
if absVal > maxAmplitude {
|
||||
maxAmplitude = absVal
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize to 0.0 - 1.0 (max for int16 is 32767)
|
||||
level := maxAmplitude / 32767.0
|
||||
r.OnVolume(level)
|
||||
}
|
||||
|
||||
// writeWAVHeader writes a standard RIFF WAV header to the output file.
|
||||
func (r *AudioRecorder) writeWAVHeader(dataSize uint32) error {
|
||||
sampleRate := uint32(SampleRate)
|
||||
@@ -310,4 +326,4 @@ func (r *AudioRecorder) writeWAVHeader(dataSize uint32) error {
|
||||
binary.Write(r.outputFile, binary.LittleEndian, dataSize)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ type Listener struct {
|
||||
hk *xhk.Hotkey
|
||||
stopModPoll chan struct{}
|
||||
mu sync.RWMutex
|
||||
eventLoopDone chan struct{}
|
||||
}
|
||||
|
||||
// NewListener creates a new hotkey listener with the specified shortcut and callbacks.
|
||||
@@ -144,11 +145,22 @@ func (l *Listener) stopListeningLocked() {
|
||||
}
|
||||
l.hk = nil
|
||||
}
|
||||
// Wait for event loop to fully terminate before allowing re-registration
|
||||
if l.eventLoopDone != nil {
|
||||
<-l.eventLoopDone
|
||||
l.eventLoopDone = nil
|
||||
}
|
||||
l.isListening = false
|
||||
}
|
||||
|
||||
// eventLoop runs the main keyboard event processing loop.
|
||||
func (l *Listener) eventLoop(hk *xhk.Hotkey) {
|
||||
done := make(chan struct{})
|
||||
l.mu.Lock()
|
||||
l.eventLoopDone = done
|
||||
l.mu.Unlock()
|
||||
defer close(done)
|
||||
|
||||
var isRecording bool
|
||||
|
||||
for {
|
||||
@@ -159,7 +171,7 @@ func (l *Listener) eventLoop(hk *xhk.Hotkey) {
|
||||
}
|
||||
if !isRecording {
|
||||
logger.Info("Shortcut activated: starting recording")
|
||||
go l.startCallback()
|
||||
l.startCallback()
|
||||
isRecording = true
|
||||
} else {
|
||||
// We received a second Keydown while already recording.
|
||||
@@ -171,9 +183,9 @@ func (l *Listener) eventLoop(hk *xhk.Hotkey) {
|
||||
// Genuine second press. Toggle off.
|
||||
logger.Info("Shortcut activated again: toggling recording (Wayland toggle fallback)")
|
||||
if l.stopCallback != nil {
|
||||
go l.stopCallback()
|
||||
l.stopCallback()
|
||||
} else {
|
||||
go l.startCallback()
|
||||
l.startCallback()
|
||||
}
|
||||
isRecording = false
|
||||
}
|
||||
@@ -196,10 +208,10 @@ func (l *Listener) eventLoop(hk *xhk.Hotkey) {
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
// Key was genuinely physically released
|
||||
logger.Info("Shortcut released: stopping recording")
|
||||
go l.stopCallback()
|
||||
l.stopCallback()
|
||||
isRecording = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,11 @@ func Error(format string, args ...interface{}) {
|
||||
log("ERROR", format, args...)
|
||||
}
|
||||
|
||||
// Debug logs a debug message with timestamp.
|
||||
func Debug(format string, args ...interface{}) {
|
||||
log("DEBUG", format, args...)
|
||||
}
|
||||
|
||||
// log writes a formatted log message to the log file and console.
|
||||
func log(level, format string, args ...interface{}) {
|
||||
logMutex.Lock()
|
||||
@@ -103,7 +108,9 @@ func log(level, format string, args ...interface{}) {
|
||||
|
||||
if logFile != nil {
|
||||
logFile.WriteString(logLine)
|
||||
logFile.Sync()
|
||||
// Do NOT call Sync() on every write — that's a massive performance
|
||||
// bottleneck (forces fsync to disk for every single log line).
|
||||
// The kernel will flush buffered writes in its own time.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,4 +148,4 @@ func getLogPath() (string, error) {
|
||||
}
|
||||
|
||||
return filepath.Join(homeDir, ".wis-free-v3", "logs", time.Now().Format("2006-01-02")+".log"), nil
|
||||
}
|
||||
}
|
||||
@@ -338,6 +338,36 @@ func (hk *Hotkey) registerPortal() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// safeSendKeydown sends a keydown event to the hotkey channel, recovering from panic
|
||||
// if the channel has been closed (e.g. during hotkey re-registration).
|
||||
func (hk *Hotkey) safeSendKeydown() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debug("safeSendKeydown: recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case hk.keydownIn <- Event{}:
|
||||
default:
|
||||
// Channel buffer is full or closed; skip.
|
||||
}
|
||||
}
|
||||
|
||||
// safeSendKeyup sends a keyup event to the hotkey channel, recovering from panic
|
||||
// if the channel has been closed (e.g. during hotkey re-registration).
|
||||
func (hk *Hotkey) safeSendKeyup() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debug("safeSendKeyup: recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case hk.keyupIn <- Event{}:
|
||||
default:
|
||||
// Channel buffer is full or closed; skip.
|
||||
}
|
||||
}
|
||||
|
||||
func (hk *Hotkey) portalSignalLoop() {
|
||||
defer close(hk.portalDone)
|
||||
|
||||
@@ -388,15 +418,15 @@ func (hk *Hotkey) portalSignalLoop() {
|
||||
|
||||
switch sig.Name {
|
||||
case ifaceGlobalShortcuts + ".Activated":
|
||||
go func() { hk.keydownIn <- Event{} }()
|
||||
hk.safeSendKeydown()
|
||||
case ifaceGlobalShortcuts + ".Deactivated":
|
||||
go func() { hk.keyupIn <- Event{} }()
|
||||
hk.safeSendKeyup()
|
||||
default:
|
||||
// Fallback for different bus routing names just in case
|
||||
if strings.HasSuffix(sig.Name, ".Activated") {
|
||||
go func() { hk.keydownIn <- Event{} }()
|
||||
hk.safeSendKeydown()
|
||||
} else if strings.HasSuffix(sig.Name, ".Deactivated") {
|
||||
go func() { hk.keyupIn <- Event{} }()
|
||||
hk.safeSendKeyup()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -428,4 +458,4 @@ func (hk *Hotkey) cleanupPortal() error {
|
||||
_ = conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user