The installation script was failing with "Permission denied" when trying to create the config directory in another user's home directory. Fixes: 1. Added home directory existence check before creating config 2. Use sudo -u to create directory as target user when needed 3. Use sudo tee to write config file (handles all permission scenarios) 4. Use sudo for sed command to modify the created config file 5. Always set proper ownership and permissions after creation This fixes the "mkdir: cannot create directory '/home/kiosk': Permission denied" error. Now the script will: - Verify target user's home directory exists (fail gracefully if not) - Create config directory as the target user - Write config file with sudo to avoid permission issues - Set proper ownership (user:user) and permissions (755 dir, 644 file)
785 lines
25 KiB
Bash
785 lines
25 KiB
Bash
#!/usr/bin/env bash
|
|
# ======================================================================
|
|
# File: install_talkkonnect_x86_complete.sh
|
|
# Purpose: Complete Talkkonnect installation for Ubuntu 24.04 x86_64
|
|
# Fixes Opus architecture issues and creates working config
|
|
# Author: Compiled from troubleshooting session
|
|
# Date: November 2025
|
|
# ======================================================================
|
|
set -e
|
|
|
|
echo "======================================================================="
|
|
echo "TalkKonnect Complete Installation Script for x86_64"
|
|
echo "This script will:"
|
|
echo " 1. Install system dependencies"
|
|
echo " 2. Install Go 1.24.1"
|
|
echo " 3. Clone and patch TalkKonnect for x86_64"
|
|
echo " 4. Build the binary"
|
|
echo " 5. Create configuration files"
|
|
echo " 6. Set up systemd service"
|
|
echo "======================================================================="
|
|
echo ""
|
|
|
|
# Check if this is a Chromebook or multi-user audio setup
|
|
echo "Audio Setup Detection:"
|
|
echo " - If you have audio working under a different user (e.g. 'kiosk'),"
|
|
echo " talkkonnect should run as that user to share the PipeWire session"
|
|
echo " - This also enables audio ducking between applications"
|
|
echo ""
|
|
read -p "Run talkkonnect as a different user? [kiosk]: " TARGET_USER
|
|
TARGET_USER=${TARGET_USER:-kiosk}
|
|
|
|
if [ "$TARGET_USER" != "$USER" ]; then
|
|
# Verify the target user exists
|
|
if ! id "$TARGET_USER" &>/dev/null; then
|
|
echo "[!] Error: User '$TARGET_USER' does not exist"
|
|
exit 1
|
|
fi
|
|
|
|
# Get target user's UID for XDG_RUNTIME_DIR
|
|
TARGET_UID=$(id -u "$TARGET_USER")
|
|
TARGET_HOME=$(eval echo ~"$TARGET_USER")
|
|
echo "[+] Will install for user: $TARGET_USER (UID: $TARGET_UID)"
|
|
echo "[+] Home directory: $TARGET_HOME"
|
|
else
|
|
TARGET_UID=$(id -u)
|
|
TARGET_HOME="$HOME"
|
|
echo "[+] Installing for current user: $USER"
|
|
fi
|
|
|
|
echo ""
|
|
read -p "Press Enter to continue or Ctrl-C to abort..."
|
|
|
|
# --- System Prep ------------------------------------------------------
|
|
echo ""
|
|
echo "[+] Updating system packages..."
|
|
sudo apt update
|
|
sudo apt upgrade -y
|
|
|
|
echo "[+] Installing dependencies..."
|
|
sudo apt install -y wget git build-essential pkg-config \
|
|
libasound2-dev libopus-dev libopus0 libopusfile-dev \
|
|
libpipewire-0.3-dev libevdev-dev libopenal-dev alsa-utils
|
|
|
|
# --- Go Installation --------------------------------------------------
|
|
echo ""
|
|
echo "[+] Installing Go 1.24.1..."
|
|
sudo rm -rf /usr/local/go /usr/lib/go* /usr/bin/go
|
|
wget https://go.dev/dl/go1.24.1.linux-amd64.tar.gz
|
|
sudo tar -C /usr/local -xzf go1.24.1.linux-amd64.tar.gz
|
|
sudo ln -sf /usr/local/go/bin/go /usr/bin/go
|
|
rm -f go1.24.1.linux-amd64.tar.gz
|
|
export PATH="/usr/local/go/bin:$PATH"
|
|
go version
|
|
|
|
# --- Clone TalkKonnect ------------------------------------------------
|
|
echo ""
|
|
echo "[+] Cloning TalkKonnect repository..."
|
|
cd ~
|
|
if [ -d "talkkonnect" ]; then
|
|
echo "[!] Removing existing talkkonnect directory..."
|
|
rm -rf talkkonnect
|
|
fi
|
|
|
|
git clone https://github.com/talkkonnect/talkkonnect.git
|
|
cd talkkonnect
|
|
|
|
echo "[+] Current version:"
|
|
git log --oneline -1
|
|
|
|
# --- Critical Fix: Patch gopus for x86_64 -----------------------------
|
|
echo ""
|
|
echo "======================================================================="
|
|
echo "[+] APPLYING X86_64 OPUS FIX"
|
|
echo "======================================================================="
|
|
echo "[+] The talkkonnect gopus package embeds Opus source for ARM"
|
|
echo "[+] but it's incomplete for x86_64. We'll patch it to use system libopus"
|
|
echo ""
|
|
|
|
# Create vendor directory
|
|
go mod vendor
|
|
|
|
# Check that gopus exists in vendor
|
|
if [ ! -d "vendor/github.com/talkkonnect/gopus" ]; then
|
|
echo "[!] ERROR: gopus not found in vendor!"
|
|
exit 1
|
|
fi
|
|
|
|
echo "[+] Backing up original opus_nonshared.go..."
|
|
cp vendor/github.com/talkkonnect/gopus/opus_nonshared.go \
|
|
vendor/github.com/talkkonnect/gopus/opus_nonshared.go.original
|
|
|
|
echo "[+] Creating x86_64-compatible opus_nonshared.go..."
|
|
cat > vendor/github.com/talkkonnect/gopus/opus_nonshared.go << 'EOFOPUS'
|
|
// +build amd64,cgo 386,cgo
|
|
|
|
package gopus
|
|
|
|
// #cgo pkg-config: opus
|
|
// #cgo LDFLAGS: -lm
|
|
//
|
|
// #include <stdio.h>
|
|
// #include <stdlib.h>
|
|
// #include <opus.h>
|
|
//
|
|
// enum {
|
|
// gopus_ok = OPUS_OK,
|
|
// gopus_bad_arg = OPUS_BAD_ARG,
|
|
// gopus_small_buffer = OPUS_BUFFER_TOO_SMALL,
|
|
// gopus_internal = OPUS_INTERNAL_ERROR,
|
|
// gopus_invalid_packet = OPUS_INVALID_PACKET,
|
|
// gopus_unimplemented = OPUS_UNIMPLEMENTED,
|
|
// gopus_invalid_state = OPUS_INVALID_STATE,
|
|
// gopus_alloc_fail = OPUS_ALLOC_FAIL,
|
|
// };
|
|
//
|
|
// enum {
|
|
// gopus_application_voip = OPUS_APPLICATION_VOIP,
|
|
// gopus_application_audio = OPUS_APPLICATION_AUDIO,
|
|
// gopus_restricted_lowdelay = OPUS_APPLICATION_RESTRICTED_LOWDELAY,
|
|
// gopus_bitrate_max = OPUS_BITRATE_MAX,
|
|
// };
|
|
//
|
|
// void gopus_setvbr(OpusEncoder *encoder, int vbr) {
|
|
// opus_encoder_ctl(encoder, OPUS_SET_VBR(vbr));
|
|
// }
|
|
//
|
|
// void gopus_setbitrate(OpusEncoder *encoder, int bitrate) {
|
|
// opus_encoder_ctl(encoder, OPUS_SET_BITRATE(bitrate));
|
|
// }
|
|
//
|
|
// opus_int32 gopus_bitrate(OpusEncoder *encoder) {
|
|
// opus_int32 bitrate;
|
|
// opus_encoder_ctl(encoder, OPUS_GET_BITRATE(&bitrate));
|
|
// return bitrate;
|
|
// }
|
|
//
|
|
// void gopus_setapplication(OpusEncoder *encoder, int application) {
|
|
// opus_encoder_ctl(encoder, OPUS_SET_APPLICATION(application));
|
|
// }
|
|
//
|
|
// opus_int32 gopus_application(OpusEncoder *encoder) {
|
|
// opus_int32 application;
|
|
// opus_encoder_ctl(encoder, OPUS_GET_APPLICATION(&application));
|
|
// return application;
|
|
// }
|
|
//
|
|
// void gopus_encoder_resetstate(OpusEncoder *encoder) {
|
|
// opus_encoder_ctl(encoder, OPUS_RESET_STATE);
|
|
// }
|
|
//
|
|
// void gopus_decoder_resetstate(OpusDecoder *decoder) {
|
|
// opus_decoder_ctl(decoder, OPUS_RESET_STATE);
|
|
// }
|
|
import "C"
|
|
|
|
import (
|
|
"errors"
|
|
"unsafe"
|
|
)
|
|
|
|
type Application int
|
|
|
|
const (
|
|
Voip Application = C.gopus_application_voip
|
|
Audio Application = C.gopus_application_audio
|
|
RestrictedLowDelay Application = C.gopus_restricted_lowdelay
|
|
)
|
|
|
|
const (
|
|
BitrateMaximum = C.gopus_bitrate_max
|
|
)
|
|
|
|
type Encoder struct {
|
|
data []byte
|
|
cEncoder *C.struct_OpusEncoder
|
|
}
|
|
|
|
func NewEncoder(sampleRate, channels int, application Application) (*Encoder, error) {
|
|
encoder := &Encoder{}
|
|
encoder.data = make([]byte, int(C.opus_encoder_get_size(C.int(channels))))
|
|
encoder.cEncoder = (*C.struct_OpusEncoder)(unsafe.Pointer(&encoder.data[0]))
|
|
|
|
ret := C.opus_encoder_init(encoder.cEncoder, C.opus_int32(sampleRate), C.int(channels), C.int(application))
|
|
if err := getErr(ret); err != nil {
|
|
return nil, err
|
|
}
|
|
return encoder, nil
|
|
}
|
|
|
|
func (e *Encoder) Encode(pcm []int16, frameSize, maxDataBytes int) ([]byte, error) {
|
|
pcmPtr := (*C.opus_int16)(unsafe.Pointer(&pcm[0]))
|
|
|
|
data := make([]byte, maxDataBytes)
|
|
dataPtr := (*C.uchar)(unsafe.Pointer(&data[0]))
|
|
|
|
encodedC := C.opus_encode(e.cEncoder, pcmPtr, C.int(frameSize), dataPtr, C.opus_int32(len(data)))
|
|
encoded := int(encodedC)
|
|
|
|
if encoded < 0 {
|
|
return nil, getErr(C.int(encodedC))
|
|
}
|
|
return data[0:encoded], nil
|
|
}
|
|
|
|
func (e *Encoder) SetVbr(vbr bool) {
|
|
var cVbr C.int
|
|
if vbr {
|
|
cVbr = 1
|
|
} else {
|
|
cVbr = 0
|
|
}
|
|
C.gopus_setvbr(e.cEncoder, cVbr)
|
|
}
|
|
|
|
func (e *Encoder) SetBitrate(bitrate int) {
|
|
C.gopus_setbitrate(e.cEncoder, C.int(bitrate))
|
|
}
|
|
|
|
func (e *Encoder) Bitrate() int {
|
|
return int(C.gopus_bitrate(e.cEncoder))
|
|
}
|
|
|
|
func (e *Encoder) SetApplication(application Application) {
|
|
C.gopus_setapplication(e.cEncoder, C.int(application))
|
|
}
|
|
|
|
func (e *Encoder) Application() Application {
|
|
return Application(C.gopus_application(e.cEncoder))
|
|
}
|
|
|
|
func (e *Encoder) ResetState() {
|
|
C.gopus_encoder_resetstate(e.cEncoder)
|
|
}
|
|
|
|
type Decoder struct {
|
|
data []byte
|
|
cDecoder *C.struct_OpusDecoder
|
|
channels int
|
|
}
|
|
|
|
func NewDecoder(sampleRate, channels int) (*Decoder, error) {
|
|
decoder := &Decoder{}
|
|
decoder.data = make([]byte, int(C.opus_decoder_get_size(C.int(channels))))
|
|
decoder.cDecoder = (*C.struct_OpusDecoder)(unsafe.Pointer(&decoder.data[0]))
|
|
|
|
ret := C.opus_decoder_init(decoder.cDecoder, C.opus_int32(sampleRate), C.int(channels))
|
|
if err := getErr(ret); err != nil {
|
|
return nil, err
|
|
}
|
|
decoder.channels = channels
|
|
|
|
return decoder, nil
|
|
}
|
|
|
|
func (d *Decoder) Decode(data []byte, frameSize int, fec bool) ([]int16, error) {
|
|
var dataPtr *C.uchar
|
|
if len(data) > 0 {
|
|
dataPtr = (*C.uchar)(unsafe.Pointer(&data[0]))
|
|
}
|
|
dataLen := C.opus_int32(len(data))
|
|
|
|
output := make([]int16, d.channels*frameSize)
|
|
outputPtr := (*C.opus_int16)(unsafe.Pointer(&output[0]))
|
|
|
|
var cFec C.int
|
|
if fec {
|
|
cFec = 1
|
|
} else {
|
|
cFec = 0
|
|
}
|
|
|
|
cRet := C.opus_decode(d.cDecoder, dataPtr, dataLen, outputPtr, C.int(frameSize), cFec)
|
|
ret := int(cRet)
|
|
|
|
if ret < 0 {
|
|
return nil, getErr(cRet)
|
|
}
|
|
return output[:ret*d.channels], nil
|
|
}
|
|
|
|
func (d *Decoder) ResetState() {
|
|
C.gopus_decoder_resetstate(d.cDecoder)
|
|
}
|
|
|
|
func GetSamplesPerFrame(data []byte, samplingRate int) (int, error) {
|
|
dataPtr := (*C.uchar)(unsafe.Pointer(&data[0]))
|
|
cSamplingRate := C.opus_int32(samplingRate)
|
|
cRet := C.opus_packet_get_samples_per_frame(dataPtr, cSamplingRate)
|
|
return int(cRet), nil
|
|
}
|
|
|
|
func CountFrames(data []byte) (int, error) {
|
|
dataPtr := (*C.uchar)(unsafe.Pointer(&data[0]))
|
|
cLen := C.opus_int32(len(data))
|
|
|
|
cRet := C.opus_packet_get_nb_frames(dataPtr, cLen)
|
|
if err := getErr(cRet); err != nil {
|
|
return 0, err
|
|
}
|
|
return int(cRet), nil
|
|
}
|
|
|
|
var (
|
|
ErrBadArgument = errors.New("bad argument")
|
|
ErrSmallBuffer = errors.New("buffer is too small")
|
|
ErrInternal = errors.New("internal error")
|
|
ErrInvalidPacket = errors.New("invalid packet")
|
|
ErrUnimplemented = errors.New("unimplemented")
|
|
ErrInvalidState = errors.New("invalid state")
|
|
ErrAllocFail = errors.New("allocation failed")
|
|
ErrUnknown = errors.New("unknown error")
|
|
)
|
|
|
|
func getErr(code C.int) error {
|
|
switch code {
|
|
case C.gopus_ok:
|
|
return nil
|
|
case C.gopus_bad_arg:
|
|
return ErrBadArgument
|
|
case C.gopus_small_buffer:
|
|
return ErrSmallBuffer
|
|
case C.gopus_internal:
|
|
return ErrInternal
|
|
case C.gopus_invalid_packet:
|
|
return ErrInvalidPacket
|
|
case C.gopus_unimplemented:
|
|
return ErrUnimplemented
|
|
case C.gopus_invalid_state:
|
|
return ErrInvalidState
|
|
case C.gopus_alloc_fail:
|
|
return ErrAllocFail
|
|
default:
|
|
return ErrUnknown
|
|
}
|
|
}
|
|
EOFOPUS
|
|
|
|
echo "[+] Patched opus_nonshared.go to use system libopus"
|
|
|
|
# --- Build TalkKonnect ------------------------------------------------
|
|
echo ""
|
|
echo "======================================================================="
|
|
echo "[+] BUILDING TALKKONNECT"
|
|
echo "======================================================================="
|
|
cd ~/talkkonnect/cmd/talkkonnect
|
|
|
|
export CGO_ENABLED=1
|
|
export CGO_CFLAGS="$(pkg-config --cflags opus)"
|
|
export CGO_LDFLAGS="$(pkg-config --libs opus) -lm"
|
|
|
|
echo "[+] Building (this may take a few minutes)..."
|
|
go build -mod=vendor -v -o ~/talkkonnect-binary . 2>&1 | tee /tmp/talkkonnect_build.log
|
|
|
|
if [ ! -f ~/talkkonnect-binary ]; then
|
|
echo ""
|
|
echo "[!] BUILD FAILED!"
|
|
echo "[!] Last 50 lines of build log:"
|
|
tail -50 /tmp/talkkonnect_build.log
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo "[+] Installing binary..."
|
|
|
|
# Stop any running talkkonnect instances first
|
|
if systemctl is-active --quiet talkkonnect 2>/dev/null; then
|
|
echo "[+] Stopping existing talkkonnect service..."
|
|
sudo systemctl stop talkkonnect
|
|
fi
|
|
|
|
# Kill any stray processes
|
|
if pgrep -x talkkonnect > /dev/null; then
|
|
echo "[+] Killing running talkkonnect processes..."
|
|
sudo pkill -9 talkkonnect
|
|
sleep 1
|
|
fi
|
|
|
|
# Now install the binary
|
|
sudo cp ~/talkkonnect-binary /usr/local/bin/talkkonnect
|
|
sudo chmod +x /usr/local/bin/talkkonnect
|
|
rm ~/talkkonnect-binary
|
|
|
|
echo "[+] Binary installed successfully!"
|
|
echo "[+] Checking dependencies:"
|
|
ldd /usr/local/bin/talkkonnect | grep -E "(opus|alsa)" || echo " (may be statically linked)"
|
|
|
|
# --- User Permissions -------------------------------------------------
|
|
echo ""
|
|
echo "[+] Setting up user permissions..."
|
|
|
|
# Add target user to input group for keyboard PTT
|
|
if ! groups "$TARGET_USER" | grep -q input; then
|
|
sudo usermod -a -G input "$TARGET_USER"
|
|
echo "[+] Added $TARGET_USER to 'input' group"
|
|
if [ "$TARGET_USER" = "$USER" ]; then
|
|
echo "[!] IMPORTANT: Log out and back in for this to take effect!"
|
|
echo "[!] Or run: newgrp input"
|
|
NEEDS_RELOGIN=true
|
|
fi
|
|
else
|
|
echo "[+] User $TARGET_USER already in 'input' group"
|
|
NEEDS_RELOGIN=false
|
|
fi
|
|
|
|
# Add target user to audio group
|
|
if ! groups "$TARGET_USER" | grep -q audio; then
|
|
sudo usermod -a -G audio "$TARGET_USER"
|
|
echo "[+] Added $TARGET_USER to 'audio' group"
|
|
fi
|
|
|
|
# --- Configuration ----------------------------------------------------
|
|
echo ""
|
|
echo "======================================================================="
|
|
echo "[+] CREATING CONFIGURATION"
|
|
echo "======================================================================="
|
|
|
|
# Verify target user's home directory exists
|
|
if [ ! -d "$TARGET_HOME" ]; then
|
|
echo "[!] Error: Home directory for user '$TARGET_USER' does not exist: $TARGET_HOME"
|
|
echo "[!] Please ensure the user account is properly set up with a home directory"
|
|
echo "[!] You may need to run: sudo mkhomedir_helper $TARGET_USER"
|
|
exit 1
|
|
fi
|
|
|
|
CONFIG_DIR="$TARGET_HOME/.config/talkkonnect"
|
|
|
|
# Create config directory with appropriate permissions
|
|
if [ "$TARGET_USER" != "$USER" ]; then
|
|
# Need to use sudo to create directory in another user's home
|
|
sudo -u "$TARGET_USER" mkdir -p "$CONFIG_DIR"
|
|
echo "[+] Created config directory as user: $TARGET_USER"
|
|
else
|
|
mkdir -p "$CONFIG_DIR"
|
|
echo "[+] Created config directory"
|
|
fi
|
|
|
|
# Create config file (use tee with sudo to handle permissions)
|
|
sudo tee "$CONFIG_DIR/talkkonnect.xml" > /dev/null << 'EOFXML'
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<document type="talkkonnect/xml">
|
|
<global>
|
|
<software>
|
|
<settings outputdevice="default"
|
|
logfilenameandpath="/home/user/.config/talkkonnect/talkkonnect.log"
|
|
logging="both"
|
|
daemonize="false"
|
|
cancelconnect="false"
|
|
simplexwithvox="false"
|
|
nextserverindex="0"/>
|
|
<autoprovisioning enabled="false"/>
|
|
</software>
|
|
<hardware>
|
|
<settings targetboard="pc"
|
|
voiceactivitytimersecs="200"/>
|
|
<io>
|
|
<pins enabled="false"/>
|
|
</io>
|
|
</hardware>
|
|
</global>
|
|
|
|
<accounts>
|
|
<account name="default" default="true">
|
|
<serverandport>your.mumble.server:64738</serverandport>
|
|
<username>your_username</username>
|
|
<password>your_password</password>
|
|
<insecure>false</insecure>
|
|
<register>false</register>
|
|
<certificate></certificate>
|
|
<channel>Root</channel>
|
|
<ident></ident>
|
|
<tokens enabled="false"></tokens>
|
|
<voicetargets enabled="false">
|
|
<id id="1" iscurrent="false" name="default">
|
|
<channels></channels>
|
|
<users></users>
|
|
</id>
|
|
</voicetargets>
|
|
</account>
|
|
</accounts>
|
|
|
|
<beacon enabled="false"/>
|
|
|
|
<audio>
|
|
<input>
|
|
<settings enabled="true"
|
|
device="default"
|
|
samplerate="48000"
|
|
channels="1"
|
|
codec="opus"
|
|
framespersecond="50"/>
|
|
</input>
|
|
<output>
|
|
<settings enabled="true"
|
|
device="default"
|
|
samplerate="48000"
|
|
channels="1"/>
|
|
</output>
|
|
</audio>
|
|
|
|
<ptt enabled="false">
|
|
<usbkeyboard enabled="false"/>
|
|
</ptt>
|
|
|
|
<voiceactivity enabled="true">
|
|
<settings threshold="0.3"
|
|
holdtimems="1000"
|
|
holdtimeoutms="2000"/>
|
|
</voiceactivity>
|
|
|
|
<sounds enabled="false"/>
|
|
|
|
<txtts enabled="false"/>
|
|
|
|
<smtp enabled="false"/>
|
|
|
|
<api enabled="false"/>
|
|
|
|
<printxml enabled="false"/>
|
|
|
|
</document>
|
|
EOFXML
|
|
|
|
# Update the log path to use actual username (needs sudo since file was created with tee)
|
|
sudo sed -i "s|/home/user/|$TARGET_HOME/|g" "$CONFIG_DIR/talkkonnect.xml"
|
|
|
|
# Set proper ownership - always needed since we used sudo tee
|
|
sudo chown -R "$TARGET_USER:$TARGET_USER" "$CONFIG_DIR"
|
|
sudo chmod 755 "$CONFIG_DIR"
|
|
sudo chmod 644 "$CONFIG_DIR/talkkonnect.xml"
|
|
echo "[+] Set ownership of config directory to $TARGET_USER"
|
|
|
|
echo "[+] Created configuration file: $CONFIG_DIR/talkkonnect.xml"
|
|
|
|
# --- Audio Mixer Detection --------------------------------------------
|
|
echo ""
|
|
echo "[+] Detecting audio configuration for user: $TARGET_USER..."
|
|
|
|
# Detect audio devices as the target user
|
|
if [ "$TARGET_USER" != "$USER" ]; then
|
|
# Get target user's runtime directory
|
|
AUDIO_DEVICES=$(sudo -u "$TARGET_USER" XDG_RUNTIME_DIR=/run/user/$TARGET_UID aplay -l 2>/dev/null)
|
|
PIPEWIRE_SINKS=$(sudo -u "$TARGET_USER" XDG_RUNTIME_DIR=/run/user/$TARGET_UID pactl list sinks short 2>/dev/null)
|
|
else
|
|
AUDIO_DEVICES=$(aplay -l 2>/dev/null)
|
|
PIPEWIRE_SINKS=$(pactl list sinks short 2>/dev/null)
|
|
fi
|
|
|
|
# Determine best audio device
|
|
AUDIO_DEVICE="default"
|
|
AUDIO_BACKEND="alsa"
|
|
|
|
if echo "$AUDIO_DEVICES" | grep -q "card 0:"; then
|
|
# ALSA devices found
|
|
CARD_NAME=$(echo "$AUDIO_DEVICES" | grep "card 0:" | head -1 | sed 's/.*card 0: \([^[]*\).*/\1/' | xargs)
|
|
echo "[+] Found ALSA card: $CARD_NAME"
|
|
|
|
# Use hw:0,0 for direct ALSA access (better for Chromebooks)
|
|
AUDIO_DEVICE="hw:0,0"
|
|
echo "[+] Using ALSA device: $AUDIO_DEVICE"
|
|
|
|
elif echo "$PIPEWIRE_SINKS" | grep -v "auto_null" | grep -q "alsa_output"; then
|
|
# PipeWire/PulseAudio sinks found
|
|
SINK_NAME=$(echo "$PIPEWIRE_SINKS" | grep -v "auto_null" | grep "alsa_output" | head -1 | awk '{print $2}')
|
|
echo "[+] Found PipeWire sink: $SINK_NAME"
|
|
AUDIO_DEVICE="pulse"
|
|
AUDIO_BACKEND="pulse"
|
|
echo "[+] Using PulseAudio/PipeWire backend"
|
|
else
|
|
echo "[!] Warning: No specific audio device detected, using 'default'"
|
|
fi
|
|
|
|
# Try to unmute and set volume
|
|
MIXER_CONTROL=$(sudo -u "$TARGET_USER" amixer scontrols 2>/dev/null | grep -oP "Simple mixer control '\K[^']+(?=',0)" | head -1)
|
|
|
|
if [ -n "$MIXER_CONTROL" ]; then
|
|
echo "[+] Found mixer control: $MIXER_CONTROL"
|
|
|
|
# Try to unmute and set volume
|
|
sudo -u "$TARGET_USER" amixer set "$MIXER_CONTROL" unmute 2>/dev/null && echo "[+] Unmuted $MIXER_CONTROL" || echo "[!] Could not unmute $MIXER_CONTROL"
|
|
sudo -u "$TARGET_USER" amixer set "$MIXER_CONTROL" 80% 2>/dev/null && echo "[+] Set $MIXER_CONTROL to 80%" || echo "[!] Could not set volume"
|
|
else
|
|
echo "[!] Warning: No ALSA mixer controls found"
|
|
echo "[!] Audio may work through PipeWire, but you may need to configure it manually"
|
|
fi
|
|
|
|
echo "[+] Audio configuration summary:"
|
|
echo " Backend: $AUDIO_BACKEND"
|
|
echo " Device: $AUDIO_DEVICE"
|
|
if [ -n "$CARD_NAME" ]; then
|
|
echo " Card: $CARD_NAME"
|
|
fi
|
|
|
|
# --- Systemd Service --------------------------------------------------
|
|
echo ""
|
|
echo "[+] Creating systemd service for user: $TARGET_USER..."
|
|
SERVICE_FILE="/etc/systemd/system/talkkonnect.service"
|
|
|
|
sudo tee "$SERVICE_FILE" > /dev/null <<EOFSVC
|
|
[Unit]
|
|
Description=TalkKonnect Headless Mumble Transceiver
|
|
After=network-online.target sound.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=$TARGET_USER
|
|
Group=$TARGET_USER
|
|
WorkingDirectory=$TARGET_HOME
|
|
ExecStart=/usr/local/bin/talkkonnect -config $CONFIG_DIR/talkkonnect.xml
|
|
Restart=always
|
|
RestartSec=5
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
# Use target user's PipeWire/PulseAudio session
|
|
Environment="XDG_RUNTIME_DIR=/run/user/$TARGET_UID"
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOFSVC
|
|
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable talkkonnect
|
|
|
|
echo "[+] Systemd service created and enabled"
|
|
|
|
# --- Audio Ducking Setup (Optional) -----------------------------------
|
|
if [ "$TARGET_USER" != "$USER" ]; then
|
|
echo ""
|
|
echo "[+] Setting up audio ducking for user: $TARGET_USER..."
|
|
echo "[+] This will lower other audio when talkkonnect is transmitting"
|
|
|
|
# Create a script to enable ducking
|
|
DUCKING_SCRIPT="$TARGET_HOME/.config/talkkonnect/enable-ducking.sh"
|
|
cat > /tmp/enable-ducking.sh << 'EOFDUCKING'
|
|
#!/bin/bash
|
|
# Enable audio ducking - lowers music/media when on PTT
|
|
pactl load-module module-role-ducking \
|
|
trigger_roles=phone \
|
|
ducking_roles=music,video \
|
|
volume=30% 2>/dev/null || echo "Ducking already enabled"
|
|
EOFDUCKING
|
|
|
|
if [ "$TARGET_USER" != "$USER" ]; then
|
|
sudo cp /tmp/enable-ducking.sh "$DUCKING_SCRIPT"
|
|
sudo chown "$TARGET_USER:$TARGET_USER" "$DUCKING_SCRIPT"
|
|
sudo chmod +x "$DUCKING_SCRIPT"
|
|
else
|
|
cp /tmp/enable-ducking.sh "$DUCKING_SCRIPT"
|
|
chmod +x "$DUCKING_SCRIPT"
|
|
fi
|
|
rm /tmp/enable-ducking.sh
|
|
|
|
echo "[+] Ducking script created: $DUCKING_SCRIPT"
|
|
echo "[+] Run as $TARGET_USER to enable: $DUCKING_SCRIPT"
|
|
fi
|
|
|
|
# --- Completion -------------------------------------------------------
|
|
echo ""
|
|
echo "======================================================================="
|
|
echo "✓✓✓ INSTALLATION COMPLETE! ✓✓✓"
|
|
echo "======================================================================="
|
|
echo ""
|
|
|
|
if [ "$TARGET_USER" != "$USER" ]; then
|
|
echo "⚠️ IMPORTANT: Talkkonnect is configured to run as user: $TARGET_USER"
|
|
echo " This allows it to share the PipeWire audio session with your apps"
|
|
echo " All configuration files are in: $TARGET_HOME/.config/talkkonnect/"
|
|
echo ""
|
|
fi
|
|
|
|
if [ "$NEEDS_RELOGIN" = true ]; then
|
|
echo "⚠️ IMPORTANT: You were added to the 'input' group"
|
|
echo " You must LOG OUT and LOG BACK IN for keyboard PTT to work!"
|
|
echo " (Or run: newgrp input)"
|
|
echo ""
|
|
fi
|
|
|
|
echo "BEFORE RUNNING: Edit the configuration file with your Mumble server details"
|
|
echo ""
|
|
if [ "$TARGET_USER" != "$USER" ]; then
|
|
echo " 1. Edit config file (as $TARGET_USER or with sudo):"
|
|
echo " sudo nano $CONFIG_DIR/talkkonnect.xml"
|
|
else
|
|
echo " 1. Edit config file:"
|
|
echo " nano $CONFIG_DIR/talkkonnect.xml"
|
|
fi
|
|
echo ""
|
|
echo " 2. Update these settings:"
|
|
echo " - <serverandport>: Your Mumble server address and port"
|
|
echo " - <username>: Your Mumble username"
|
|
echo " - <password>: Your Mumble password (if required)"
|
|
echo " - <channel>: Channel to join on connect"
|
|
echo " - <insecure>: Set to 'true' if server has self-signed cert"
|
|
echo ""
|
|
echo " 3. Optional - Enable keyboard/USB button PTT (voice activation is enabled by default):"
|
|
echo " - Set <ptt enabled=\"true\">"
|
|
echo " - For USB mini keyboard: Find device with 'sudo evtest'"
|
|
echo " - Set <usbkeyboard enabled=\"true\" device=\"/dev/input/eventX\" keycode=\"KEY_F13\"/>"
|
|
echo " - Tip: Program your USB keyboard to send F13-F24 to avoid conflicts"
|
|
echo " - Set <voiceactivity enabled=\"false\"/>"
|
|
if [ "$TARGET_USER" = "$USER" ]; then
|
|
echo " - Make sure you're in 'input' group: groups | grep input"
|
|
fi
|
|
echo ""
|
|
echo "TESTING:"
|
|
if [ "$TARGET_USER" != "$USER" ]; then
|
|
echo " Test manually as $TARGET_USER:"
|
|
echo " sudo -u $TARGET_USER /usr/local/bin/talkkonnect -config $CONFIG_DIR/talkkonnect.xml"
|
|
else
|
|
echo " Test manually first:"
|
|
echo " /usr/local/bin/talkkonnect -config $CONFIG_DIR/talkkonnect.xml"
|
|
fi
|
|
echo ""
|
|
echo " Once working, start the service:"
|
|
echo " sudo systemctl start talkkonnect"
|
|
echo ""
|
|
echo " Check status:"
|
|
echo " sudo systemctl status talkkonnect"
|
|
echo ""
|
|
echo " View logs:"
|
|
echo " journalctl -u talkkonnect -f"
|
|
echo " cat $CONFIG_DIR/talkkonnect.log"
|
|
echo ""
|
|
if [ "$TARGET_USER" != "$USER" ]; then
|
|
echo "AUDIO DUCKING:"
|
|
echo " To enable audio ducking (lower other apps when transmitting):"
|
|
echo " sudo -u $TARGET_USER $CONFIG_DIR/enable-ducking.sh"
|
|
echo ""
|
|
fi
|
|
echo "FILES:"
|
|
echo " Binary: /usr/local/bin/talkkonnect"
|
|
echo " Config: $CONFIG_DIR/talkkonnect.xml"
|
|
echo " Service: /etc/systemd/system/talkkonnect.service"
|
|
echo " Source: ~/talkkonnect"
|
|
echo " Logs: $CONFIG_DIR/talkkonnect.log"
|
|
if [ "$TARGET_USER" != "$USER" ]; then
|
|
echo " Ducking: $CONFIG_DIR/enable-ducking.sh"
|
|
fi
|
|
echo ""
|
|
echo "TROUBLESHOOTING:"
|
|
echo " - Build log: /tmp/talkkonnect_build.log"
|
|
echo " - Runtime logs: $CONFIG_DIR/talkkonnect.log"
|
|
echo " - System logs: journalctl -u talkkonnect -f"
|
|
echo " - If audio issues: check 'aplay -L' and 'arecord -L'"
|
|
echo " - If 'unable to unmute' error: run 'amixer scontrols' to see controls"
|
|
echo " - If cert errors: set <insecure>true</insecure> in config"
|
|
echo " - For keyboard PTT: ensure you're in 'input' group (groups)"
|
|
echo " - If crash after connect: ensure voicetargets section exists in XML"
|
|
echo ""
|
|
echo "KNOWN ISSUES:"
|
|
echo " - Voice activation is enabled by default (PTT disabled)"
|
|
echo " - Keyboard PTT requires logout/login after installation"
|
|
echo " - Self-signed certs need <insecure>true</insecure>"
|
|
if [ "$TARGET_USER" != "$USER" ]; then
|
|
echo " - Config files owned by $TARGET_USER - edit with sudo or su"
|
|
echo " - Audio ducking must be enabled manually (see above)"
|
|
fi
|
|
echo ""
|
|
echo "CHROMEBOOK-SPECIFIC:"
|
|
echo " - If no audio: ensure $TARGET_USER can access /dev/snd devices"
|
|
echo " - Check audio as target user: sudo -u $TARGET_USER aplay -l"
|
|
echo " - PipeWire session: /run/user/$TARGET_UID/pipewire-0"
|
|
echo ""
|
|
echo "======================================================================="
|