#!/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 // #include // #include // // 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 "=======================================================================" CONFIG_DIR="$HOME/.config/talkkonnect" mkdir -p "$CONFIG_DIR" cat > "$CONFIG_DIR/talkkonnect.xml" << 'EOFXML' your.mumble.server:64738 your_username your_password false false Root EOFXML # Update the log path to use actual username sed -i "s|/home/user/|$HOME/|g" "$CONFIG_DIR/talkkonnect.xml" 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 < /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 " - : Your Mumble server address and port" echo " - : Your Mumble username" echo " - : Your Mumble password (if required)" echo " - : Channel to join on connect" echo " - : 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 " echo " - For USB mini keyboard: Find device with 'sudo evtest'" echo " - Set " echo " - Tip: Program your USB keyboard to send F13-F24 to avoid conflicts" echo " - Set " 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 true 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 true" 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 "======================================================================="