#!/usr/bin/python3 -u

# autojack - Monitors dbus for added audio devices (hot plugged USB audio intefaces)
# on detect it does one of three things:
# makes it the jack device
# makes it a jack client (via zita-ajbridge)
# nothing
#
# Monitors acpi for headphone un/plug  and reroutes audio if needed
#
# autojack also monitors dbus for messages from studio-controls to:
# - stop jack
# - re/start jack
# - remove a USB device as jack master to allow safe device removal
# - reread ~/.config/autojackrc and apply any changes

import alsaaudio
import copy
import dbus
import dbus.mainloop.glib
import glob
import jack
import json
import logging
import os
import re
import shlex
import signal
import subprocess
import sys
import time

from gi.repository import GLib
from os.path import expanduser

global name_base
global control_interface_name
global configure_interface_name
global service_name
name_base = 'org.jackaudio'
control_interface_name = name_base + '.JackControl'
configure_interface_name = name_base + '.Configure'
service_name = name_base + '.service'


def get_dev_info(x):
    '''uses audio device number to make add a device structure with device
    info to the config database
    '''

    global conf_db
    global fw_exists
    fw_exists = False
    cname = ""
    sub = 0
    usb = False
    usbid = "none"
    usbbus = "none"
    if os.path.exists(f"/proc/asound/card{str(x)}"):
        if os.path.isfile(f"/proc/asound/card{str(x)}/id"):
            with open(f"/proc/asound/card{str(x)}/id", "r") as card_file:
                for line in card_file:
                    # only need one line
                    cname = line.rstrip()
        else:
            cname = str(x)
    if os.path.exists(f"/proc/asound/card{str(x)}/usbbus"):
        usb = True
        with open(f"/proc/asound/card{str(x)}/usbbus", "r") as card_file:
            for line in card_file:
                # only need one line
                usbbus = line.rstrip()
        with open(f"/proc/asound/card{str(x)}/usbid", "r") as card_file:
            for line in card_file:
                # only need one line
                usbid = line.rstrip()
        #device['usb'] = True

    dname = "none"
    for ddev in conf_db['devices']:
        if usb:
            # for usb devices where there may be more than one of same model manufacture
            # so we look for both id and bus to distinguish
            if [usbid, usbbus] == [conf_db['devices'][ddev]['id'], conf_db['devices'][ddev]['bus']]:
                conf_db['devices'][ddev]['raw'] = cname
        if conf_db['devices'][ddev]['raw'] == cname:
            dname = ddev
    if dname != "none":
        conf_db['devices'][dname]['number'] = x
    else:
        # device not found in data base
        if usb:
            # what is the next available usb* number?
            dname = f"USB{conf_db['extra']['usbnext']}"
            conf_db['extra']['usbnext'] += 1
        else:
            dname = cname
        conf_db['devices'][dname] = {'number': x, 'id': usbid, 'bus': usbbus}

    device = conf_db['devices'][dname]
    device['min_latency'] = 16
    device['raw'] = cname
    device['usb'] = usb
    device['internal'] = os.path.exists(f"/proc/asound/card{str(x)}/codec#0")
    device['firewire'] = os.path.exists(f"/proc/asound/card{str(x)}/firewire")
    device['hdmi'] = False
    if cname == "HDMI" or cname == "NVidia":
        device['hdmi'] = True
    rates = ['32000', '44100', '48000', '88200', '96000', '192000']
    if device['internal']:
        device['min_latency'] = 128
        with open(f"/proc/asound/card{str(x)}/codec#0", "r") as card_file:
            node = False
            rates = []
            for line in card_file:
                if 'Node' in line:
                    node = True
                    if 'Digital' in line:
                        node = False
                if node and 'rates' in line:
                    fnd_rates = line.split()[2:]
                    for rate in fnd_rates:
                        if not rate in rates:
                            rates.append(rate)

    if device['usb']:
        device['min_latency'] = 32
        if os.path.exists(f"/proc/asound/card{str(x)}/stream0"):
            with open(f"/proc/asound/card{str(x)}/stream0", "r") as card_file:
                rates = []
                for line in card_file:
                    if 'Rates:' in line:
                        fnd_rates = line.split()[1:]
                        for rate in fnd_rates:
                            rate = rate.split(',')[0]
                            if not rate in rates:
                                rates.append(rate)

    device['rates'] = rates
    fw_exists = device['firewire']

    if not 'sub' in device:
        device['sub'] = {}

    for y in range(0, 20):

        subdevice = {}
        cap = False
        cap_pid = 0
        play = False
        play_pid = 0
        if os.path.exists(f"/proc/asound/card{str(x)}/pcm{str(y)}p"):
            play = True
            if os.path.exists(f"/proc/asound/card{str(x)}/pcm{str(y)}p/sub0"):
                with open(f"/proc/asound/card{str(x)}/pcm{str(y)}p/sub0/status", "r") as info_file:
                    for line in info_file:
                        if re.match("^owner_pid", line.rstrip()):
                            play_pid = int(line.rstrip().split(": ", 1)[1])

        if os.path.exists(f"/proc/asound/card{str(x)}/pcm{str(y)}c"):
            cap = True
            if os.path.exists(f"/proc/asound/card{str(x)}/pcm{str(y)}c/sub0"):
                with open(f"/proc/asound/card{str(x)}/pcm{str(y)}c/sub0/status", "r") as info_file:
                    for line in info_file:
                        if re.match("^owner_pid", line.rstrip()):
                            cap_pid = int(line.rstrip().split(": ", 1)[1])

        if play or cap:
            if not str(y) in device['sub']:
                # Rate and frame should be same as jack unless the device does not support that
                thisrate = conf_db['jack']['rate']
                if not conf_db['jack']['rate'] in device['rates']:
                    if 48000 in device['rates']:
                        thisrate = 48000
                    elif 44100 in device['rates']:
                        thisrate = 44100
                    else:
                        thisrate = device['rates'][0]
                thisframe = conf_db['jack']['frame']
                if thisframe < device['min_latency']:
                    thisframe = device['min_latency']
                if device['usb']:
                    device['sub'][str(y)] = {'playback': True, 'capture': True,
                            'play-chan': 100, 'cap-chan': 100,
                            'rate': thisrate, 'frame': thisframe,
                            'nperiods': conf_db['jack']['period'], 'hide': False,
                            'name': f"{device['raw']},{str(y)},0", 'cap-latency': 0,
                            'play-latency': 0}
                else:
                    device['sub'][str(y)] = {'playback': True, 'capture': True,
                            'play-chan': 0, 'cap-chan': 0,
                            'rate': thisrate, 'frame': thisframe,
                            'nperiods': conf_db['jack']['period'], 'hide': False,
                            'name': f"{device['raw']},{str(y)},0", 'cap-latency': 0,
                            'play-latency': 0}

            device['sub'][str(y)]['playback'] = play
            device['sub'][str(y)]['capture'] = cap
            device['sub'][str(y)]['play-pid'] = play_pid
            device['sub'][str(y)]['cap-pid'] = cap_pid
    logging.log(7, f"Device scan found: {str(cname)}")
    #logging.log(7, f"Device list: {str(device)}")
    #logging.log(7, json.dumps(device, indent = 4))


def import_device_array():
    '''creates an array of device structures as per get_dev_info(), from
    device 0 to the highest number device currently on the system. Missing
    device numbers will still save a space in the case a USB device has
    been removed and there is still a higher number so that array index
    can be the same as the device number.'''
    global conf_db
    devices = conf_db['devices']
    ndevs = 0
    for this_dev in devices:
        # we need these even for unplugged devices
        # devices[this_dev]['firewire'] = False
        devices[this_dev]['number'] = -1
        for sub in devices[this_dev]['sub']:
            if not 'play-pid' in devices[this_dev]['sub'][sub]:
                devices[this_dev]['sub'][sub]['play-pid'] = 0
            if not 'cap-pid' in devices[this_dev]['sub'][sub]:
                devices[this_dev]['sub'][sub]['cap-pid'] = 0

    if os.path.exists("/proc/asound/cards"):
        with open("/proc/asound/cards", "r") as cards_file:
            for line in cards_file:
                # need to find lines with:space/int/space[
                # ndevs = int from above
                # last one is highest dev number
                sub = line.rstrip()[1:]
                sub2 = sub.split(" ")
                if sub2[0].isdigit():
                    ndevs = int(sub2[0])
    ndevs += 1
    for x in range(0, ndevs):
        # card loop
        get_dev_info(x)


def get_raw(device):
    ''' given a USB*,*,* device name return a
    true (raw) dev,*,* string'''
    global conf_db
    if device == 'none':
        return 'none'
    un, ud, us = device.split(',')
    if un in conf_db['devices']:
        rn = conf_db['devices'][un]['raw']
        return f"{rn},{ud},{us}"
    else:
        return 'none'


def get_db(device):
    ''' give a device string (raw or db nameed)
    return the data base for that device'''
    global conf_db
    name = device.split(',')[0]
    if name in conf_db['devices']:
        return conf_db['devices'][name]
    else:
        for dev in conf_db['devices']:
            if conf_db['devices'][dev]['raw'] == name:
                return conf_db['devices'][dev]


def extra_devices():
    ''' set up all extra device as per configuration '''
    global conf_db
    global last_master
    global phones
    logging.debug("updating extra devices")

    if not conf_db['jack']['on']:
        return
        # no use checking anything else
    import_device_array()
    # a few loops
    for dev in conf_db['devices']:
        rawdev = conf_db['devices'][dev]['raw']
        numst = conf_db['devices'][dev]['number']
        usb = bool(conf_db['devices'][dev]['usb'] and conf_db['extra']['usbauto'])
        for sub in conf_db['devices'][dev]['sub']:
            fullname = f"{dev},{sub},0"
            sub_db = conf_db['devices'][dev]['sub'][sub]
            logging.debug(f"checking: {fullname} channel counts: {sub_db['play-chan']} {sub_db['cap-chan']} card: {numst}")
            if sub_db['hide'] or int(conf_db['devices'][dev]['number']) == -1:
                if sub_db['cap-pid'] or sub_db['play-pid']:
                    kill_slave(fullname)
                break
            if get_raw(fullname) == last_master:
                break
            if fullname == conf_db['extra']['phone-device']:
                if phones:
                    if not sub_db['play-pid']:
                        start_slave(fullname)
                        break
            if sub_db['play-chan'] or sub_db['cap-chan']:
                if not sub_db['cap-pid'] or not sub_db['play-pid']:
                    start_slave(fullname)
                    break
            else:
                if sub_db['cap-pid'] or sub_db['play-pid']:
                    kill_slave(fullname)
    logging.debug("updating extra devices complete")


def net_bridge():
    ''' set up all audio network bridges as per configuration '''
    global conf_db
    logging.debug("updating Audio network bridges")

    if not conf_db['jack']['on']:
        return
        # no use checking anything else
    cp = subprocess.run(["/usr/bin/killall", "zita-n2j"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    cp1 = subprocess.run(["/usr/bin/killall", "zita-j2n"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"kill old zita-njbridge: {cp.stdout.strip()}{cp1.stdout.strip()}")
    for bridge in conf_db['znet']:
        br_db = conf_db['znet'][bridge]
        cmd = ""
        cnt_str = "--chan "
        late_bits = ""
        if br_db['direction'] == "out":
            cmd = "zita-j2n "
            cnt_str = f"{cnt_str}{str(br_db['count'])}"
            late_bits = f"--{br_db['bits']}"
        elif br_db['direction'] == "in":
            cmd = "zita-n2j"
            late_bits = f"--buff {str(br_db['latency'])}"
            for i in range(1, (br_db['count'] + 1)):
                if i == br_db['count']:
                    cnt_str = f"{cnt_str}{str(i)}"
                else:
                    cnt_str = f"{cnt_str}{str(i)},"
        else:
            logging.warning(f"audio network bridge {bridge} invalid")
            continue

        cmd = f"{cmd} --jname {bridge} {cnt_str} {late_bits} {br_db['ip']} {str(br_db['port'])}"
        subprocess.Popen(shlex.split(cmd), shell=False).pid


    logging.debug("updating network audio bridges complete")


def import_config(caller):
    ''' sets default parmeters, then reads values from configuration file'''
    global config
    global config_path
    global config_file
    global conf_db
    global install_path

    logging.log(7, f"import_config called by: {caller}")

    config_path = "~/.config/autojack"
    config_file = f"{config_path}/autojack.json"

    default_device = "0,0,0"
    for adir in glob.glob("/proc/asound/card*/codec*"):
        idfile = adir.rsplit('/', 1)[0]
        with open(f"{idfile}/id", "r") as card_file:
            for line in card_file:
                # only need one line
                tmpname = line.rstrip()
            if tmpname != "HDMI" and tmpname != "NVidia":
                default_device = f"{tmpname},0,0"

    # read in autojack config file
    c_file = expanduser(config_file)
    if not os.path.isfile(c_file):
        # no config file, make one
        cp = subprocess.run([f"{install_path}/bin/convert-studio-controls"],
            universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"convert config file: {cp.stdout.strip()}")

    # config file exists, read it in
    with open(c_file) as f:
        conf_db = json.load(f)
    #logging.log(7, json.dumps(conf_db, indent = 4))
    # print(json.dumps(conf_db, indent = 4))

    for pout in conf_db['pulse']['outputs']:
        if conf_db['pulse']['outputs'][pout]['connection'] == "monitor":
            conf_db['pulse']['outputs'][pout]['connection'] = conf_db['extra']['monitor']

    if conf_db['jack']['driver'] == 'firewire':
        #we don't save this, but treat usbdev as none
        conf_db['jack']['usbdev'] = 'none'
    logger = logging.getLogger()
    logger.setLevel(int(conf_db['log-level']))
    logging.log(7, f"log level: {str(conf_db['log-level'])}")
    import_device_array()
    logging.log(7, json.dumps(conf_db, indent = 4))


def reconfig():
    '''reads values from configuration file and changes run to match. This tries
    to do this without stopping jack if not needed'''
    global phones
    global conf_db
    global last_master
    global midiproc
    logging.debug("reconfigure autojack")

    old_conf_db = copy.deepcopy(conf_db)
    import_config("reconfig")
    ol_jkdb = old_conf_db['jack']
    jackdb = conf_db['jack']

    old_jack = [ol_jkdb['on'], ol_jkdb['driver'], ol_jkdb['dev'],
            ol_jkdb['rate'], ol_jkdb['frame'], ol_jkdb['period'],
            ol_jkdb['connect-mode'], ol_jkdb['chan-in'], ol_jkdb['chan-out'],
            ol_jkdb['usbdev'], ol_jkdb['cap-latency'], ol_jkdb['play-latency']]
    new_jack = [jackdb['on'], jackdb['driver'], jackdb['dev'],
            jackdb['rate'], jackdb['frame'], jackdb['period'],
            jackdb['connect-mode'], jackdb['chan-in'], jackdb['chan-out'],
            jackdb['usbdev'], jackdb['cap-latency'], jackdb['play-latency']]
    if old_jack != new_jack:
        logging.debug("change requires restart")
        config_start()
        return
    if not conf_db['jack']['on']:
        return
        # no use checking anything else

    pulse_dirty = False
    if old_conf_db['pulse']['inputs'] != conf_db['pulse']['inputs']:
        logging.debug("reconfiguring pulse bridges")
        pulse_dirty = True
        cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-jack-source"],
                            universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"remove jackd_source: {cp.stdout.strip()}")
        for bridge in conf_db['pulse']['inputs']:
            this_count = str(conf_db['pulse']['inputs'][bridge]['count'])
            cp = subprocess.run(["/usr/bin/pactl", "load-module", "module-jack-source", f"client_name={bridge}",
                                f"channels={this_count}", "connect=no"],
                                universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
            logging.debug(f"Load jackd_source: {cp.stdout.strip()}")

    if old_conf_db['pulse']['outputs'] != conf_db['pulse']['outputs']:
        logging.debug("reconfiguring pulse bridges")
        if not pulse_dirty:
            pulse_dirty = True
        cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-jack-sink"],
                            universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"PA unload jack-sink: {cp.stdout.strip()}")
        for bridge in conf_db['pulse']['outputs']:
            this_count = str(conf_db['pulse']['outputs'][bridge]['count'])
            cp = subprocess.run(["/usr/bin/pactl", "load-module", "module-jack-sink", f"client_name={bridge}",
                            f"channels={this_count}", "connect=no"], universal_newlines=True, stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT, shell=False)
            logging.debug(f"PA load jack-sink: {cp.stdout.strip()}")

    if old_conf_db['extra']['a2j'] != conf_db['extra']['a2j']:
        logging.debug("reconfiguring MIDI bridge")
        if 'midiproc' in globals():
            midiproc.send_signal(signal.SIGINT)
            try:
                rt = midiproc.wait(timeout=15)
            except subprocess.TimeoutExpired:
                logging.debug(f"kill a2jmidid failed")
                try:
                    os.kill(int(sub_db['cap-pid']), 9)
                except:
                    print("")

        if conf_db['extra']['a2j']:
            # Can't add logging for background processes. Will show up in syslog
            cmd = "/usr/bin/a2jmidid -e"
            midiproc = subprocess.Popen(shlex.split(cmd), shell=False)

    if [old_conf_db['mnet']['count'], old_conf_db['mnet']['type']] != [conf_db['mnet']['count'], conf_db['mnet']['type']]:
        logging.debug("reconfiguring MIDI network")
        cp = subprocess.run(["/usr/bin/killall", "qmidinet"],
                            universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"kill old qmidinet: {cp.stdout.strip()}")
        if conf_db['mnet']['count']:
            mtype = ""
            if conf_db['mnet']['type'] == 'jack':
                mtype = "--alsa-midi=no --jack-midi=yes"
            cmd = f"/usr/bin/qmidinet --num-ports={str(conf_db['mnet']['count'])} --no-gui {mtype}"
            subprocess.Popen(shlex.split(cmd), shell=False).pid


    # extra devices compares what is to config and fixes things
    logging.debug("Check extra devices")
    extra_devices()
    net_bridge()

    if conf_db['extra']['phone-device'] != old_conf_db['extra']['phone-device']:
        logging.debug("reconfigure Headphones setup")
        phones = False
        phones_check()
    if conf_db['extra']['monitor'] != "system:playback_1" and not phones:
        logging.debug("Check main outputs")
        mon_dev, ldev, temp = conf_db['extra']['monitor'].split('-')
        if not conf_db['devices'][mon_dev]['sub'][ldev] and conf_db['jack']['driver']  != "alsa" or mon_dev != last_master:
            start_slave(mon_dev)
            con_dirty = True

    if pulse_dirty:
        #do this last after all bridges are created
        connect_pa()


def config_start():
    ''' Pulls configuration and force restarts the world '''
    global conf_db
    global configure_interface_name
    global control_interface_name
    global fw_exists
    global jack_alive
    global jack_died
    global last_master
    global midiproc
    global name_base
    global phones
    global service_name

    if jack_alive:
        kill_jack_client()

    logging.info("Running: config_start()")

    import_config("config_start")
    # if at session start we should wait a few seconds for pulse
    # to be fully running
    time.sleep(2)
    logging.debug("Running: config_start jack stat")
    jack_stat("Stopping...")
    logging.info("Running: config_start after jack_stat")
    fpath = expanduser('~/.config/autojack/prestop')
    if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
        logging.debug("Found prestop script")
        cp = subprocess.run([fpath],
                    universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
        logging.debug(f"Running script: {fpath}")
    if 'midiproc' in globals():
        midiproc.send_signal(signal.SIGINT)
        try:
            rt = midiproc.wait(timeout=15)
        except subprocess.TimeoutExpired:
            logging.debug(f"kill a2jmidid failed")
            try:
                os.kill(int(sub_db['cap-pid']), 9)
            except:
                print("")

    bus = dbus.SessionBus()
    controller = bus.get_object(service_name, "/org/jackaudio/Controller")
    control_iface = dbus.Interface(controller, control_interface_name)
    configure_iface = dbus.Interface(controller, configure_interface_name)

    try:
        control_iface.StopServer()
    except:
        print("stop failed")
    time.sleep(2)

    # Stop jack if running
    cp = subprocess.run(["/usr/bin/killall", "-q", "-9", "jackdbus", "jackd", "a2jmidid"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Kill old Procs: {cp.stdout.strip()}")
    jack_stat("Stopped")
    last_master = 'none'

    fpath = expanduser('~/.config/autojack/poststop')
    if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
        logging.debug("Found poststop script")
        cp = subprocess.run([fpath],
                    universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
        logging.debug(f"Running script: {fpath}")

    if (conf_db['jack']['driver'] == "firewire") or fw_exists:
        time.sleep(3)
        # this is a firewire device, A busreset makes for stability
        cp = subprocess.run(["/usr/bin/ffado-test", "BusReset"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"reset firewire bus: {cp.stdout.strip()}")
        time.sleep(3)
        cp = subprocess.run(["/usr/bin/killall", "-q", "ffado-dbus-server", "ffado-mixer"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Kill ffado mixer because busreset needed: {cp.stdout.strip()}")

    if not conf_db['jack']['on']:
        # restart Pulse
        cp = subprocess.run(["/usr/bin/pulseaudio", "-k"],
                    universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"JACK is {str(conf_db['jack']['on'])} restart pulse: {cp.stdout.strip()}")
        return
    logging.debug(f"JACK is {str(conf_db['jack']['on'])} start up jack")
    jack_stat("Config Pulse")

    # Assume start of session where pulse may be fully loaded
    # get rid of anything that can automatically interfere
    cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-jackdbus-detect"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"remove jackdbus_detect: {cp.stdout.strip()}")
    cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-udev-detect"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"remove module-udev-detect: {cp.stdout.strip()}")
    cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-alsa-card"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"remove module-alsa-card: {cp.stdout.strip()}")

    fpath = expanduser('~/.config/autojack/prestart')
    if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
        logging.debug("Found prestart script")
        jack_stat("Prestart running")
        cp = subprocess.run([fpath],
                    universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
        logging.debug(f"Running script: {fpath}")

    jack_stat("Configuring...")
    rn = 'none'
    if conf_db['jack']['usbdev'] != "none":
        #mdev = conf_db['jack']['usbdev']
        rn = get_db(conf_db['jack']['usbdev'])['raw']
    if os.path.exists(f"/proc/asound/{rn}"):
        mdev = get_raw(conf_db['jack']['usbdev'])
    else:
        mdev = conf_db['jack']['dev']
        if mdev.split(',')[0] == "HDMI" or mdev.split(',')[0] == "NVidia":
            logging.info("HDMI device, setting buffer to 4096")
            conf_db['jack']['frame'] = "4096"
    mdev_db = get_db(mdev)

    if os.path.isfile(expanduser('~/.config/jack/conf.xml')):
        logging.debug("Found previous jack config removing")
        os.remove(expanduser('~/.config/jack/conf.xml'))

    # Now start jackdbus with the configured device
    if conf_db['jack']['connect-mode'] != 'n':
        bus = dbus.SessionBus()
        controller = bus.get_object(service_name, "/org/jackaudio/Controller")
        control_iface = dbus.Interface(controller, control_interface_name)
        configure_iface = dbus.Interface(controller, configure_interface_name)
        configure_iface.SetParameterValue(['engine', 'self-connect-mode'],
            dbus.Byte(ord(conf_db['jack']['connect-mode'])))

    if conf_db['jack']['driver'] == "firewire":
        cp = subprocess.run(["/usr/bin/ffado-test", "BusReset"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"reset firewire bus: {cp.stdout.strip()}")
        cp = subprocess.run(["/usr/bin/killall", "-q", "ffado-dbus-server", "ffado-mixer"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Kill ffado mixer because busreset needed: {cp.stdout.strip()}")
        time.sleep(3)

    bus = dbus.SessionBus()
    controller = bus.get_object(service_name, "/org/jackaudio/Controller")
    control_iface = dbus.Interface(controller, control_interface_name)
    configure_iface = dbus.Interface(controller, configure_interface_name)

    configure_iface.SetParameterValue(['engine', 'driver'], str(conf_db['jack']['driver']))
    logging.debug(f"JACK driver set to: {conf_db['jack']['driver']}")
    time.sleep(.4)
    if conf_db['jack']['driver'] == "alsa":
        # we use default device for FW right now
        configure_iface.SetParameterValue(['driver', 'device'], f"hw:{mdev}")
        logging.debug(f"JACK device set to: {mdev}")
        if mdev_db['firewire'] and conf_db['jack']['frame'] < 256:
            conf_db['jack']['frame'] = 256
            logging.info(f"Firewire device using ALSA drivers, JACK buffer set to: 256")

    time.sleep(.2)
    configure_iface.SetParameterValue(['driver', 'rate'], dbus.UInt32(conf_db['jack']['rate']))
    time.sleep(.2)
    configure_iface.SetParameterValue(['driver', 'period'], dbus.UInt32(conf_db['jack']['frame']))
    time.sleep(.2)
    if conf_db['jack']['driver'] != "dummy":
        configure_iface.SetParameterValue(['driver', 'nperiods'],
            dbus.UInt32(conf_db['jack']['period']))
        time.sleep(.2)
        configure_iface.SetParameterValue(['driver', 'input-latency'],
            dbus.UInt32(conf_db['jack']['cap-latency']))
        time.sleep(.2)
        configure_iface.SetParameterValue(['driver', 'output-latency'],
            dbus.UInt32(conf_db['jack']['play-latency']))

    else:
        configure_iface.SetParameterValue(['driver', 'capture'],
            dbus.UInt32(conf_db['jack']['chan-in']))
        time.sleep(.2)
        configure_iface.SetParameterValue(['driver', 'playback'],
            dbus.UInt32(conf_db['jack']['chan_out']))
        time.sleep(.2)
        configure_iface.SetParameterValue(['driver', 'monitor'], True)
    logging.debug(f"JACK rate/period/nperiods set to: {conf_db['jack']['rate']}/{conf_db['jack']['frame']}/{conf_db['jack']['period']}")
    time.sleep(3)
    jack_stat("Starting...")

    try:
        control_iface.StartServer()
    except:
        logging.debug("JACK start failed")
        jack_stat("Start Failed")
        return
    logging.debug("JACK started")

    if conf_db['jack']['driver'] == "alsa":
        last_master = mdev
    # maybe check for jack up (need function?)
    time.sleep(3)

    jack_stat("Poststart")
    fpath = expanduser('~/.config/autojack/poststart')
    if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
        logging.debug("Found poststart script")
        cp = subprocess.run([fpath],
                    universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
        logging.debug(f"Running script: {fpath}")

    start_jack_client()
    time.sleep(1)

    jack_stat("Adding Bridges")
    for bridge in conf_db['pulse']['inputs']:
        this_count = str(conf_db['pulse']['inputs'][bridge]['count'])
        cp = subprocess.run(["/usr/bin/pactl", "load-module", "module-jack-source",
                f"client_name={bridge}", f"channels={this_count}", "connect=no"],
                universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Pulseaudio: {cp.stdout.strip()}")
    for bridge in conf_db['pulse']['outputs']:
        this_count = str(conf_db['pulse']['outputs'][bridge]['count'])
        cp = subprocess.run(["/usr/bin/pactl", "load-module", "module-jack-sink",
                f"client_name={bridge}", f"channels={this_count}", "connect=no"],
                universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Pulseaudio: {cp.stdout.strip()}")

    extra_devices()
    net_bridge()

    # check if phones device exists
    ph_dev = conf_db['extra']['phone-device'].split(',')[0]
    if not ph_dev in conf_db['devices']:
        phones = False
    if phones:
        if conf_db['extra']['phone-device'] != last_master:
            start_slave(conf_db['extra']['phone-device'])
            con_dirty = True
    elif conf_db['extra']['monitor'] != "system:playback_1":
        mon_dev = conf_db['extra']['monitor'].split('-')[0]
        if not mon_dev in zdev and (conf_db['jack']['driver'] != "alsa" or mon_dev != last_master):
            start_slave(mon_dev)
            con_dirty = True

    # not sure all these delays need to be here. Was checking with old pulse.
    # this has to be last after all audio bridges are created
    time.sleep(2)
    connect_pa()

    if conf_db['extra']['a2j']:
        # Can't set up logging
        cmd = "/usr/bin/a2jmidid -e"
        midiproc = subprocess.Popen(shlex.split(cmd), shell=False)

    if conf_db['mnet']['count']:
        mtype = ""
        if conf_db['mnet']['type'] == 'jack':
            mtype = "--alsa-midi=no --jack-midi=yes"
        cmd = f"/usr/bin/qmidinet --num-ports={str(conf_db['mnet']['count'])} --no-gui {mtype}"
        subprocess.Popen(shlex.split(cmd), shell=False).pid

    jack_stat("Postbridge")
    fpath = expanduser('~/.config/autojack/postbridge')
    if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
        logging.debug("Found poststart script")
        cp = subprocess.run([fpath],
                    universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
        logging.debug(f"Running script: {fpath}")
    jack_stat("Running")


def start_jack_client():
    ''' Create a jack client for monitoring and changing port connections'''
    global jack_client
    global jack_died
    global jack_alive
    global con_dirty
    logging.debug("create jack client")
    jack.set_error_function(callback=jack_error)
    jack.set_info_function(callback=jack_info)
    try:
        jack_client = jack.Client('AutoJack', use_exact_name=False, no_start_server=True)
    except jack.JackError:
        logging.warning("Unable to create jack client")
        jack_died = True
        return
    jack_client.set_shutdown_callback(jackdied)
    jack_client.set_port_connect_callback(jack_con_det)
    jack_client.activate()

    jack_died = False
    jack_alive = True
    con_dirty = True
    logging.debug("jack client created")


def kill_jack_client():
    global jack_client
    global jack_alive
    global jack_died

    if jack_client:
        jack_client.deactivate()
        jack_client.close()
    jack.set_error_function()
    jack.set_info_function()
    jack_died = False
    jack_alive = False
    jack_stat("Stopped")
    logging.debug("jack client closed and cleaned up")


def jackdied(state, why):
    '''gets called by jack if it is exiting, we can't clean up
    here... so tell the world jack died with a flag instead'''
    global jack_died
    jack_died = True
    jack_stat("Stopping")
    logging.debug("jack died callback")


def jack_stat(status):
    global config_path
    global ping_string
    statfile =  expanduser(f"{config_path}/autojack.state")
    with open(statfile, "w") as st_file:
        st_file.write(status)
    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/",
                    f"org.studio.control.event.{ping_string}"], shell=False)

def jack_con_det(a, b, connect):
    ''' a port has had a connection check if one of the ports is monitor L/R
        if so and phones == True move to phones port'''
    global con_dirty
    global conf_db
    if connect and (phones or (conf_db['extra']['monitor'] != "system:playback_1")):
        con_dirty = True


def check_jack_status(user_data):
    '''Check if jack has died and the client needs to be
    closed.'''
    global jack_client
    global jack_alive
    global jack_died
    global jack_count
    global con_dirty
    global phone_port

    # sent by jackdied() callback
    if jack_died:
        kill_jack_client()

    jack_count = jack_count + 1
    if jack_count == 200:
        logging.log(7, f"Jack_status: {str(jack_alive)}")
        jack_count = 0

        logfile = expanduser("~/.log/autojack.log")
        if os.path.isfile(logfile) and os.path.getsize(logfile) > 200000:
            logging.debug("Log File getting large: rotate")
            for old_idx in range(5, -1, -1):
                if old_idx:
                    if os.path.isfile(f"{logfile}.{str(old_idx)}"):
                        os.replace(f"{logfile}.{str(old_idx)}", f"{logfile}.{str(old_idx + 1)}")
                else:
                    logging.shutdown()
                    os.replace(logfile, f"{logfile}.1")
                    logging.basicConfig(filename=logfile, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
                                        level=conf_db['log-level'])
            logging.debug("Log File rotated")
        logfile = expanduser("~/.log/jack/jackdbus.log")
        if os.path.isfile(logfile) and os.path.getsize(logfile) > 200000:
            logging.debug("JACK log File getting large: rotate")
            for old_idx in range(5, -1, -1):
                if old_idx:
                    if os.path.isfile(f"{logfile}.{str(old_idx)}"):
                        os.replace(f"{logfile}.{str(old_idx)}", f"{logfile}.{str(old_idx + 1)}")
                else:
                    os.replace(logfile, f"{logfile}.1")
            logging.debug("JACK Log File rotated")

    if con_dirty and jack_alive:
        logging.debug("Connection changed")
        if phones:
            new_l = phone_port
            logging.debug(f"change it to phones port: {new_l}")
        else:
            new_l = conf_db['extra']['monitor']
        switch_outputs("system:playback_1", new_l)
        con_dirty = False

    return True


def switch_outputs(left_old, left_new):
    '''finds any outputs connected to a_dev and moves them to b_dev.
    Devices are jack client names including the first port of the stereo
    pair. example: system:playback_1'''
    global jack_client
    global jack_alive
    logging.debug(f"moving connections from {str(left_old)} to {str(left_new)}")
    if not jack_alive:
        logging.debug("Jack not running")
        return
    if left_old == left_new:
        logging.debug("both ports are the same, no move done")
        return
    right_old = get_next_port(left_old)
    right_new = get_next_port(left_new)
    l_old_con = jack_client.get_all_connections(left_old)
    logging.log(8, f"old left connections {str(l_old_con)}")
    # first connect all output to new device
    for raw_p in l_old_con:
        s_port = str(raw_p).split("'")[1]
        jack_client.connect(s_port, left_new)
    r_old_con = jack_client.get_all_connections(right_old)
    logging.log(8, f"old right connections {str(r_old_con)}")
    for raw_p in r_old_con:
        s_port = str(raw_p).split("'")[1]
        jack_client.connect(s_port, right_new)
    # then disconnect old ones
    for raw_p in l_old_con:
        s_port = str(raw_p).split("'")[1]
        jack_client.disconnect(s_port, left_old)
    for raw_p in r_old_con:
        s_port = str(raw_p).split("'")[1]
        jack_client.disconnect(s_port, right_old)


def get_next_port(left_port_ask):
    ''' given a port, this returns the jack port of the next port
    or for a mono device the same port so that it is connected to both
    left and write'''
    global jack_client
    global jack_alive
    if not jack_alive:
        return ""

    right = False
    # For firewire backend, the ports are labeled firewire_pcm:something<num>_in or _out
    # but have an alias of system:<direction>_<num>. We have to find the real port name
    # for left_port
    left_port = jack_client.get_port_by_name(left_port_ask)
    logging.debug(f"Real left port name: {str(left_port.name)}")
    right_port = left_port.name
    if left_port.is_input:
        ports = jack_client.get_ports(f"{left_port.name.split(':')[0]}*", is_audio=True, is_input=True)
    else:
        ports = jack_client.get_ports(f"{left_port.name.split(':')[0]}*", is_audio=True, is_output=True)
    for next_port in ports:
        if next_port.name == left_port.name:
            logging.log(8, f"{str(next_port.name)} == {str(left_port.name)}")
            right = True
        elif right:
            right_port = next_port.name
            logging.log(8, f"Right port: {str(right_port)}")
            break
    return right_port


def connect_pa():
    '''connects pulse ports to the correct device ports. May have to
    use zita-ajbridge to first make the correct device available.'''
    global conf_db
    global jack_client
    global jack_alive
    if not jack_alive:
        logging.debug("Can't connect pulse, jack not running")
        return
    logging.debug("connect pulse bridges")

    for bridge in conf_db['pulse']['inputs']:
        connection = conf_db['pulse']['inputs'][bridge]['connection']
        if connection != 'none':
            prevport = ""
            nextport = connection
            pulselist = jack_client.get_ports(f"{bridge}:*", is_audio=True, is_input=True)
            for pport in pulselist:
                # apparently reg ex with : doesn't find : so sort it here
                if pport.name.split(':')[0] != bridge:
                    continue
                if nextport != prevport:
                    logging.debug(f"jack connect {nextport} to {pport.name}")
                    try:
                        jack_client.connect(nextport, pport)
                    except:
                        logging.log(8, "already connected skipping")
                    prevport = nextport
                    nextport = get_next_port(prevport)

    for bridge in conf_db['pulse']['outputs']:
        connection = conf_db['pulse']['outputs'][bridge]['connection']
        if connection != 'none':
            prevport = ""
            nextport = connection
            if connection == "monitor":
                nextport = conf_db['extra']['MONITOR']
            pulselist = jack_client.get_ports(f"{bridge}:*", is_audio=True, is_output=True)
            for pport in pulselist:
                if pport.name.split(':')[0] != bridge:
                    continue
                if nextport != prevport:
                    logging.debug(f"jack connect {pport.name} to {nextport}")
                    try:
                        jack_client.connect(pport, nextport)
                    except:
                        logging.log(8, "already connected skipping")
                    prevport = nextport
                    nextport = get_next_port(prevport)


def disconnect_pa(our_config):
    '''disconnect Pulse ports we know we have connected.
    The pa-jack bridge is left running. Leave other connections
    alone.'''
    global jack_client
    global jack_alive
    if not jack_alive:
        logging.debug("Jack not running")
        return
    logging.debug("disconnect pulse bridges")
    p_in_db = our_config['pulse']['inputs']
    p_out_db = our_config['pulse']['outputs']
    for bridge in p_in_db:
        if p_in_db[bridge]['connection'] != 'none':
            prevport = ""
            nextport = p_in_db[bridge]['connection']
            pulselist = jack_client.get_ports(f"{bridge}:*", is_audio=True, is_input=True)
            for pport in pulselist:
                if nextport != prevport:
                    logging.debug(f"jack disconnect {nextport} from {pport.name}")
                    try:
                        jack_client.disconnect(nextport, pport)
                    except:
                        logging.log(8, "already connected skipping")
                    prevport = nextport
                    nextport = get_next_port(prevport)

    for bridge in p_out_db:
        if p_out_db[bridge]['connection'] != 'none':
            prevport = ""
            if p_out_db[bridge]['connection'] == "monitor":
                nextport = our_config['extra']['monitor']
            else:
                nextport = p_out_db[bridge]['connection']
            pulselist = jack_client.get_ports(f"{bridge}:*", is_audio=True, is_output=True)
            for pport in pulselist:
                if nextport != prevport:
                    logging.debug(f"jack disconnect {pport.name} from {nextport}")
                    try:
                        jack_client.disconnect(pport, nextport)
                    except:
                        logging.log(8, "already connected skipping")
                    prevport = nextport
                    nextport = get_next_port(prevport)


def msg_cb_new(*args, **kwargs):
    '''call back for udev sensing new device. checks if device is audio.
    checks if device is USB. If both are true and configuration is to
    use this device, the device is either connected with zita-ajbridge
    or becomes jack's master device'''
    global last_master
    global conf_db

    if not conf_db['jack']['on']:
        # then we don't care
        return

    # let things settle
    time.sleep(3)
    old_devs_db = copy.deepcopy(conf_db['devices'])

    if args[0].find("sound-card") >= 0:
        # remake database
        import_device_array()
        dev_db = {}
        for dev_name in conf_db['devices']:
            dev_db = conf_db['devices'][dev_name]
            # we only care about usb devices that are plugged in and have audio
            if dev_db['usb'] and dev_db['number'] != -1 and len(dev_db['sub']):
                # check if it was not in db before or "card number" has changed
                if not dev_name in old_devs_db or dev_db['number'] != old_devs_db[dev_name]['number']:
                    # tell gui devicelist has changed so it can up date device dropdowns
                    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/", "org.studio.control.event.usb_signal"],
                                   shell=False)
                    logging.debug(f"USB device {dev_name} has been plugged in")
    
                    for subn in dev_db['sub']:
                        subname = f"{dev_name},{str(subn)},0"
                        if subname == last_master:
                            #this device is current master maybe jack did a reset?
                            continue
                        sub_db = dev_db['sub'][subn]
                        #logging.debug(f" device = {subname} play:{str(sub_db['playback'])}"
                        #            f" capture:{str(sub_db['capture'])}")
                        if (conf_db['jack']['driver'] == "alsa") and (conf_db['jack']['usbdev'] == subname):
                            logging.debug(f"Changing jack master to: {subname}")
                            config_start()
                            return
                        if sub_db['play-pid'] or sub_db['cap-pid']:
                            # device in use already
                            continue
                        if conf_db['extra']['usbauto']:
                            #extra_devices()
                            start_slave(subname)
                            if subname == conf_db['extra']['phone-device']:
                                phones_switch(True)


def msg_cb_removed(*args, **kwargs):
    ''' dbus call back when a USB device removal has been detected by udev '''
    global last_master
    global conf_db
    global jack_died

    if not conf_db['jack']['on']:
        # then we don't care
        return

    # let things settle
    time.sleep(.5)
    old_devs_db = copy.deepcopy(conf_db['devices'])
    if args[0].find("sound-card") >= 0:
        # remake database
        import_device_array()
        dev_db = {}
        for dev_name in conf_db['devices']:
            dev_db = conf_db['devices'][dev_name]
            # we only care about usb devices that are unplugged and have audio
            if dev_db['usb'] and dev_db['number'] == -1 and len(dev_db['sub']):
                # check if it was not in db before or "card number" has changed
                if dev_name in old_devs_db or dev_db['number'] != old_devs_db[dev_name]['number']:
                    # tell gui devicelist has changed so it can up date device dropdowns
                    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/", "org.studio.control.event.usb_signal"],
                                   shell=False)
                    logging.debug(f"USB device {dev_name} has been unplugged")

                    for subn in dev_db['sub']:
                        subname = f"{dev_name},{str(subn)},0"
                        if conf_db['jack']['usbdev'] == subname:
                            # must get rid of our jack client
                            #jack_died = True
                            if last_master == get_raw(conf_db['jack']['usbdev']):
                                kill_slave(conf_db['jack']['dev'])
                                time.sleep(1)
                                config_start()
                        elif conf_db['extra']['usbauto']:
                            if subname == conf_db['extra']['phone-device']:
                                phones_switch(False)
                            kill_slave(subname)


def get_card_rate(rtdev):
    ''' find out closest SR jack master that device handles'''
    global conf_db
    dname, l_dev, sub = rtdev.split(",", 2)
    dev_db = conf_db['devices'][dname]
    rtdev = f"{dev_db['raw']},{l_dev},{sub}"
    desired_rate = str(dev_db['sub'][l_dev]['rate'])
    backup_rate = ""
    if os.path.isfile(f"/proc/asound/{dev_db['raw']}/pcm{l_dev}p/sub{sub}/hw_params"):
        p_file = f"/proc/asound/{dev_db['raw']}/pcm{l_dev}p/sub{sub}/hw_params"
        dtype = 0
    elif os.path.isfile(f"/proc/asound/{dev_db['raw']}/pcm{l_dev}c/sub{sub}/hw_params"):
        p_file = f"/proc/asound/{dev_db['raw']}/pcm{l_dev}c/sub{sub}/hw_params"
        dtype = 1
    else:
        logging.warning(f"missing info file for: {rtdev} adding client may fail.")
        return desired_rate

    adev_h = alsaaudio.PCM(type=dtype, device=f"hw:{rtdev}")

    adev_h.setrate(int(desired_rate))
    with open(p_file, "r") as card_file:
        for line in card_file:
            if 'rate:' in line:
                fnd_rate = line.split()[1]
                backup_rate = fnd_rate
                if fnd_rate == desired_rate:
                    logging.debug(f"Good supports desired rate: {desired_rate}")
                    adev_h.close()
                    return fnd_rate
    adev_h.close()
    for try_rate in ['48000', '44100', '88200', '96000', '32000']:
        adev_h.setrate(int(try_rate))
        with open(p_file, "r") as card_file:
            for line in card_file:
                if 'rate:' in line:
                    fnd_rate = line.split()[1]
                    if fnd_rate == try_rate:
                        logging.debug(f"Closest we could find: {fnd_rate}")
                        adev_h.close()
                        return fnd_rate
        adev_h.close()

    if backup_rate:
        #adev_h.close()
        return backup_rate
    logging.debug(f"Unable to confirm working rate, using: {desired_rate}")
    #adev_h.close()
    return desired_rate


def start_slave(ldev):
    ''' takes the audio device as a parameter and starts a bridge
    from that device to jack '''
    global conf_db
    global procs

    logging.debug(f"Start slave: {ldev}")
    import_device_array()
    dname, l_dev, sub = ldev.split(",", 2)
    dev_db = conf_db['devices'][dname]
    sub_db = dev_db['sub'][l_dev]

    if sub_db['hide']:
        #don't start a slave blacklisted device
        logging.debug(f"Blacklisted: {ldev} don't start")
        return

    raw_xp = f"{dev_db['raw']},{l_dev},{sub}"
    buff_size = sub_db['frame']
    #should do play and capture separately
    if sub_db['play-pid'] or sub_db['cap-pid']:
        logging.debug(f" Device {ldev} already bridged or in use by other application")
        return

    if dev_db['hdmi']:
        logging.info("HDMI device, setting buffer to 4096")
        buff_size = "4096"
    elif dev_db['internal'] and (buff_size < 128):
        logging.info("Internal device, minimum buffer 128, using 128")
        buff_size = "128"
    dsr = get_card_rate(ldev)
    # we found it and it seems to have this sub
    if sub_db['playback']:
        if not sub_db['play-pid']:
            #this should detect if the user has manually set either in or out numbers
            channels = 0
            if sub_db['play-chan'] or sub_db['cap-chan']:
                channels = sub_db['play-chan']
            elif dev_db['usb'] and not conf_db['extra']['usb-single']:
                channels = 100
            elif ldev == conf_db['extra']['phone-device']:
                channels = 2
            if channels:
                cmd = f"/usr/bin/zita-j2a -j {sub_db['name']}-out -d hw:{raw_xp} -r {dsr} -p {buff_size}"
                cmd = f"{cmd} -n {str(sub_db['nperiods'])} -c {str(channels)}"
                cmd = f"{cmd} -O {str(sub_db['play-latency'])}"
                logging.debug(f"device bridging comand line: {cmd}")
                procout = subprocess.Popen(shlex.split(cmd), shell=False)
                pidout = procout.pid
                logging.debug(f" Device {ldev} out has pid: {pidout}")
                procs.append(procout)
                sub_db['play-pid'] = pidout
        else:
            logging.debug(f" Device {ldev} playback already bridged or in use by other application")
    if sub_db['capture']:
        if not sub_db['cap-pid']:
            chanels = 0
            if sub_db['play-chan'] or sub_db['cap-chan']:
                channels = sub_db['cap-chan']
            elif dev_db['usb']:
                channels = 100
            if channels:
                cmd = f"/usr/bin/zita-a2j -j {sub_db['name']}-in -d hw:{raw_xp} -r {dsr} -p {buff_size}"
                cmd = f"{cmd} -n {str(sub_db['nperiods'])} -c {str(channels)}"
                cmd = f"{cmd} -I {str(sub_db['cap-latency'])}"
                logging.debug(f"Device input comand line: {cmd}")
                procin = subprocess.Popen(shlex.split(cmd), shell=False)
                pidin = procin.pid
                logging.debug(f" Device {ldev} in has pid: {pidin}")
                procs.append(procin)
                sub_db['cap-pid'] = pidin
        else:
            logging.debug(f" Device {ldev} capture already bridged or in use by other application")
    time.sleep(1)
    import_device_array()


def kill_slave(ldev):
    ''' takes the device as a parameter and if the device exists
    and is bridged to jack, stops the bridge '''
    global conf_db
    global procs
    dname, l_dev, sub = ldev.split(",", 2)
    logging.debug(f"{ldev} kill in progress")
    dev_db = conf_db['devices'][dname]
    if len(dev_db['sub']):
        sub_db = dev_db['sub'][l_dev]
        logging.debug(f"{dname} has {str(len(dev_db['sub']))} sub devices, want sub device {l_dev}")
        if sub_db['play-pid']:
            logging.debug(f"{ldev} found Playback bridge to kill. PID: {str(sub_db['play-pid'])}")
            for i, pr in enumerate(procs):
                if pr.pid == sub_db['play-pid']:
                    logging.debug(f"kill {str(dname)} sub: {str(l_dev)} PID: {str(sub_db['play-pid'])}")
                    pr.send_signal(signal.SIGINT)
                    try:
                        rt = pr.wait(timeout=15)
                    except subprocess.TimeoutExpired:
                        logging.debug(f"kill PID: {str(sub_db['play-pid'])} failed")
                        pr.terminate()
                        outs, errs = pr.communicate()
                    del procs[i]
        else:
            logging.debug(f"{ldev} no Playback bridge found")
        if sub_db['cap-pid']:
            logging.debug(f"{ldev} found Capture bridge to kill. PID: {str(sub_db['cap-pid'])}")
            for i, pr in enumerate(procs):
                if pr.pid == sub_db['cap-pid']:
                    logging.info(f"kill {str(dname)} sub: {str(l_dev)} PID: {str(sub_db['cap-pid'])}")
                    pr.send_signal(signal.SIGINT)
                    try:
                        rt = pr.wait(timeout=15)
                    except subprocess.TimeoutExpired:
                        logging.debug(f"kill PID: {str(sub_db['cap-pid'])} failed")
                        try:
                            os.kill(int(sub_db['cap-pid']), 9)
                        except:
                            print("")
                    del procs[i]
        else:
            logging.debug(f"{ldev} no Capture bridge found")


def ses_cb_quit(*args, **kwargs):
    ''' dbus call back when quit signal caught. This is for use in
    testing and the GUI never sends this. '''
    logging.warning("Got quit signal.")
    we_die()

def handler(signum, frame):
    ''' a handler for system signals that may be sent by the system.
        we want to trap sigint, sigkill and sigterm and do the same as
        above. '''
    logging.warning(f"Got signal number: {str(signum)} - Dying.")
    we_die()

def ses_cb_stop(*args, **kwargs):
    ''' dbus call back when stop signal caught. This stops jack. '''
    logging.info("Got stop signal.")
    config_start()

def we_die():
    global lock_file
    global midiproc
    if 'midiproc' in globals():
        midiproc.send_signal(signal.SIGINT)
        try:
            rt = midiproc.wait(timeout=15)
        except subprocess.TimeoutExpired:
            logging.debug(f"kill a2jmidid failed")
            try:
                os.kill(int(sub_db['cap-pid']), 9)
            except:
                print("")
    cp = subprocess.run(["/usr/bin/killall", "-9", "jackdbus", "jackd", "a2jmidid"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    jack_stat("Stopped")
    logging.debug(f"Kill jack and friends: {cp.stdout.strip()}")
    cp = subprocess.run(["/usr/bin/pulseaudio", "-k"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Restart PA: {cp.stdout.strip()}")
    if os.path.isfile(lock_file):
        new_pid = str(os.getpid())
        if os.path.isfile(lock_file):
            with open(lock_file, "r") as lk_file:
                for line in lk_file:
                    # only need one line
                    old_pid = line.rstrip()
            if new_pid == old_pid:
                os.remove(lock_file)
    os._exit(0)


def ses_cb_start(*args, **kwargs):
    ''' dbus call back when (re)start signal caught'''
    logging.info("Got start signal.")
    config_start()


def ses_cb_config(*args, **kwargs):
    ''' dbus call back when config signal caught '''
    logging.info("Got config signal.")
    reconfig()


def ses_cb_ping(*args, **kwargs):
    ''' dbus call back when config signal caught '''
    global ping_string
    logging.info("Got ping signal.")
    time.sleep(3)
    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/", f"org.studio.control.event.{ping_string}"],
                   shell=False)


def phones_switch(plugged):
    ''' Does the actual phones signal and level switching '''
    global conf_db
    global phones
    global phone_port
    global jack_client
    global jack_alive
    phones = plugged
    logging.debug(f"Changing Headphone to plugged in = {str(plugged)}")
    if (not conf_db['jack']['on']) or not jack_alive:
        # we can't do anything up to pulse (I think)
        return
    out_L = "system:playback_1"
    out_R = "system:playback_2"
    logging.debug(f"Headphone phone action = {str(conf_db['extra']['phone-action'])}")
    dname, l_dev, sub = conf_db['extra']['phone-device'].split(",", 2)
    dev_db = conf_db['devices'][dname]
    sub_db = dev_db['sub'][l_dev]

    if conf_db['extra']['phone-action'] == "switch":
        logging.debug("Switching outputs")
        if dev_db['internal']:
            spkr = ""
            logging.debug("Headphone device is internal change it's mixer settings")
            my_mixers = alsaaudio.mixers(device=f"hw:{dname}")
            if "Front" in my_mixers:
                spkr = "Front"
            elif "Speakers" in my_mixers:
                spkr = "Speakers"
            spkr_mix = alsaaudio.Mixer(control=spkr, device=f'hw:{dname}')
            hp_mix = alsaaudio.Mixer(control='Headphone', device=f'hw:{dname}')
            if plugged:
                spkr_mix.setvolume(0)
                spkr_mix.setmute(1)
                hp_mix.setvolume(100)
                hp_mix.setmute(0)
            else:
                spkr_mix.setvolume(100)
                spkr_mix.setmute(0)
                hp_mix.setvolume(0)
                hp_mix.setmute(1)
        if conf_db['extra']['phone-device'] != conf_db['jack']['dev']:
            logging.debug("Headphone device is not system")
            # set jack port names
            phone_port = f"{conf_db['extra']['phone-device']}-out:playback_1"
            logging.debug(f"Headphone jack is: {phone_port}")
            # USB device?
            if not plugged:
                port_list = jack_client.get_ports(name_pattern=f'{dname}*', is_audio=True, is_input=True, is_physical=True)
                for this_port in port_list:
                    if this_port.name == phone_port:
                        switch_outputs(phone_port, conf_db['extra']['monitor'])
                if (not sub_db['play-chan']) and (not dev_db['usb']):
                    logging.debug("Headphone device had no jack port ... kill unneeded bridge")
                    kill_slave(conf_db['extra']['phone-device'])
            else:
                if not sub_db['play-pid'] and (not dev_db['usb']):
                    logging.debug("Headphone device has no jack port ... create bridge")
                    # we have to start a zita proc
                    start_slave(conf_db['extra']['phone-device'])
                    time.sleep(1)
                port_list = jack_client.get_ports(name_pattern=f'{dname}*', is_audio=True, is_input=True, is_physical=True)
                for this_port in port_list:
                    if this_port.name == phone_port:
                        switch_outputs("system:playback_1", phone_port)

    elif conf_db['extra']['phone-action'] == "script":
        # run a script to perform this instead
        logging.debug("Use script")
        fpath = expanduser('~/.config/autojack/headphone')
        if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
            cp = subprocess.run([fpath, str(plugged)],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
            logging.debug(f"Running script: {fpath} {str(plugged)}")
    else:
        logging.debug("No action chosen")


def phones_check():
    ''' check to see if there are phones plugged in. We only do this
    on start up'''
    logging.debug("Checking for phones plugged in")
    global phones
    global conf_db
    phones = False

    import_config("phones_check")
    import_device_array()

    dname, ddev, dsub = conf_db['extra']['phone-device'].split(',')
    dev_db = conf_db['devices'][dname]
    sub_db = dev_db['sub'][ddev]
    logging.log(7, f"phonecheck: got device")
    if int(dev_db['number']) < 0:
        logging.info(f"Phones device: {dname} not present")
        return
    logging.debug(f"Checking phones device: {dname}")
    if dev_db['usb']:
        # phones device is USB
        if sub_db['playback']:
            # If USB make sure it has audio playback
            phones_switch(True)
            return
        else:
            logging.warning(f"Headphone device {conf_db['extra']['phone-device']} appears to have no outputs")
    elif dev_db['internal']:
        # this is internal we can use amixer
        logging.debug("Checking for phones with internal device")
        my_mixers = alsaaudio.mixers(device=f"hw:{dname}")
        hp_sw = ""
        if "Front" in my_mixers:
            hp_sw = "Front "
        cmd = f"/usr/bin/amixer -D hw:{dname} cget iface=CARD,name='{hp_sw}Headphone Jack'"
        logging.debug(f"amixer command: {cmd}")
        cp = subprocess.run(shlex.split(cmd), universal_newlines=True,
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        for line in cp.stdout.split(":"):
            logging.debug(f"amixer line: {line.strip()}")
            if line.strip() == "values=on":
                phones_switch(True)
                return
            elif line.strip() == "values=off":
                phones_switch(False)
                return
        logging.debug(f"headphone detect string: {cp.stdout.strip()}")


def ses_cb_phones(*args, **kwargs):
    ''' User has manually asked to toggle phones (un)plugged'''
    global phones
    if phones:
        phones_switch(False)
        phones = False
    else:
        phones_switch(True)
        phones = True
    logging.debug(f"Manual phones switch to: {str(phones)}")


def phones_plug(*args, **kwargs):
    ''' callback means headphones have been plugged in lets make sure
        phones are unmuted and have a level higher than -inf.
        We also may want to mute speakers '''
    global conf_db
    logging.info("Got phones plugged in signal.")
    dname, ddev, dsub = conf_db['extra']['phone-device'].split(',')
    dev_db = conf_db['devices'][dname]
    if int(dev_db['number']) > -1 and dev_db['internal']:
            phones_switch(True)
    else:
        logging.info("Ignored phone plug: not correct device")


def phones_unplug(*args, **kwargs):
    ''' callback means headphones have been unplugged in lets make sure
        phones are muted and the speakers  are unmuted and have a
        level higher than -inf.'''
    global conf_db
    logging.info("Got phones unplugged signal.")
    dname, ddev, dsub = conf_db['extra']['phone-device'].split(',')
    dev_db = conf_db['devices'][dname]
    if int(dev_db['number']) > -1 and dev_db['internal']:
        phones_switch(False)
    else:
        logging.info("Ignored phone unplug: not correct device")


def jack_error(mesg):
    ''' jack call back to deal with jack errors '''
    global jack_alive
    if jack_alive:
        if "not running" in mesg:
            logging.warning(f"Jack Message: {mesg}")


def jack_info(mesg):
    ''' jack call back to deal with jack info '''
    logging.info(f"Jack Message: {mesg}")


def main():
    ''' Autojack runs at session start and manages audio for the session.
    this is the daemon for studio-controls'''
    global install_path
    global last_master
    global procs
    global jack_alive
    global jack_client
    global jack_died
    global jack_count
    global phones
    global con_dirty
    global lock_file
    global config_path
    global ping_string
    ping_string = "V3_5_signal"
    phones = False
    jack_count = 0
    jack_client = 0
    procs = []
    last_master = ""
    jack_alive = False
    jack_died = False
    con_dirty = False
    print("starting up")
    install_path = os.path.abspath(f"{os.path.dirname(sys.argv[0])}/..")
    print(f"install path: {install_path}")

    # Try and kill any other running instance
    config_path = expanduser("~/.config/autojack")
    if not os.path.isdir(config_path):
        os.makedirs(config_path)
    lock_file = f"{config_path}/autojack.lock"
    # if os.path.isfile(lock_file): # this can be added in when 20.04 is EOL
    print("sending quit to any old autojack")
    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/",
                "org.studio.control.event.quit_signal"], shell=False)
    time.sleep(3)
    new_pid = str(os.getpid())
    if os.path.isfile(lock_file):
        # other instance still hasn't gone maybe hung
        with open(lock_file, "r") as lk_file:
            for line in lk_file:
                # only need one line
                old_pid = line.rstrip()
        if new_pid != old_pid:
            print("old lock file found, killing old pid")
            try:
                os.kill(int(old_pid), 9)
            except:
                print("")
            time.sleep(1)
    with open(lock_file, "w") as lk_file:
        lk_file.write(new_pid)
        print("Lock file created")

    # set up logging
    logpath = expanduser("~/.log")
    # make sure the logfile directory exists
    if not os.path.exists(logpath):
        os.makedirs(logpath)
    logfile = expanduser("~/.log/autojack.log")
    logging.basicConfig(filename=logfile, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG)
    logging.info('Autojack started: logging started')
    print("logging started")

    cp = subprocess.run([f"{install_path}/bin/convert-studio-controls"],
        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"check config file: {cp.stdout.strip()}")

    phones_check()
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    signal.signal(signal.SIGHUP, handler)
    signal.signal(signal.SIGINT, handler)
    signal.signal(signal.SIGQUIT, handler)
    signal.signal(signal.SIGILL, handler)
    signal.signal(signal.SIGTRAP, handler)
    signal.signal(signal.SIGABRT, handler)
    signal.signal(signal.SIGBUS, handler)
    signal.signal(signal.SIGFPE, handler)
    #signal.signal(signal.SIGKILL, handler) unhandlable  :)
    signal.signal(signal.SIGUSR1, handler)
    signal.signal(signal.SIGSEGV, handler)
    signal.signal(signal.SIGUSR2, handler)
    signal.signal(signal.SIGPIPE, handler)
    signal.signal(signal.SIGALRM, handler)
    signal.signal(signal.SIGTERM, handler)

    system_bus = dbus.SystemBus()
    system_bus.add_signal_receiver(msg_cb_new, dbus_interface='org.freedesktop.systemd1.Manager', signal_name='UnitNew')
    system_bus.add_signal_receiver(msg_cb_removed, dbus_interface='org.freedesktop.systemd1.Manager',
                                   signal_name='UnitRemoved')
    system_bus.add_signal_receiver(phones_plug, dbus_interface='org.studio.control.event',
                                signal_name='plug_signal')
    system_bus.add_signal_receiver(phones_unplug, dbus_interface='org.studio.control.event',
                                signal_name='unplug_signal')
    user_bus = dbus.SessionBus()
    user_bus.add_signal_receiver(ses_cb_start, dbus_interface='org.studio.control.event',
                                 signal_name='start_signal')
    user_bus.add_signal_receiver(ses_cb_stop, dbus_interface='org.studio.control.event',
                                 signal_name='stop_signal')
    user_bus.add_signal_receiver(ses_cb_config, dbus_interface='org.studio.control.event',
                                 signal_name='config_signal')
    user_bus.add_signal_receiver(ses_cb_ping, dbus_interface='org.studio.control.event',
                                 signal_name='ping_signal')
    user_bus.add_signal_receiver(ses_cb_quit, dbus_interface='org.studio.control.event',
                                 signal_name='quit_signal')
    user_bus.add_signal_receiver(ses_cb_phones, dbus_interface='org.studio.control.event',
                                 signal_name='phones_signal')

    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/",
                    "org.studio.control.event.start_signal"], shell=False)
    jack_stat("undetermined")

    timeout_id = GLib.timeout_add(500, check_jack_status, None)

    loop = GLib.MainLoop()
    loop.run()


if __name__ == '__main__':
    main()
