diff --git a/.gitignore b/.gitignore index 326ed27f..529fa05b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ src/dist/ # Other data/templates/energyXT.xt + +# IDEs +.idea/ \ No newline at end of file diff --git a/data/cadence-pulse2jack b/data/cadence-pulse2jack index 5266cb9f..4243a0a0 100755 --- a/data/cadence-pulse2jack +++ b/data/cadence-pulse2jack @@ -1,44 +1,48 @@ -#!/bin/bash +#! /usr/bin/env bash # Script to bridge/start pulseaudio into JACK mode INSTALL_PREFIX="X-PREFIX-X" +PULSE_CONFIG_DIR=${PULSE_CONFIG_DIR:-"$HOME/.pulse"} +JACK_CONNFILE="$PULSE_CONFIG_DIR/jack-connections" +PA_CTLFILE="$PULSE_CONFIG_DIR/ctl.pa" + # ---------------------------------------------- -if [ ! -d ~/.pulse ]; then - mkdir -p ~/.pulse +if [ ! -d $PULSE_CONFIG_DIR ]; then + mkdir -p $PULSE_CONFIG_DIR fi -if [ ! -f ~/.pulse/client.conf ]; then - echo "autospawn = no" > ~/.pulse/client.conf +if [ ! -f $PULSE_CONFIG_DIR/client.conf ]; then + echo "autospawn = no" > $PULSE_CONFIG_DIR/client.conf else - if (! cat ~/.pulse/client.conf | grep "autospawn = no" > /dev/null); then - sed -i '/autospawn =/d' ~/.pulse/client.conf - echo "autospawn = no" >> ~/.pulse/client.conf + if (! cat $PULSE_CONFIG_DIR/client.conf | grep "autospawn = no" > /dev/null); then + sed -i '/autospawn =/d' $PULSE_CONFIG_DIR/client.conf + echo "autospawn = no" >> $PULSE_CONFIG_DIR/client.conf fi fi -if [ ! -f ~/.pulse/daemon.conf ]; then - echo "default-sample-format = float32le" > ~/.pulse/daemon.conf - echo "realtime-scheduling = yes" >> ~/.pulse/daemon.conf - echo "rlimit-rttime = -1" >> ~/.pulse/daemon.conf - echo "exit-idle-time = -1" >> ~/.pulse/daemon.conf +if [ ! -f $PULSE_CONFIG_DIR/daemon.conf ]; then + echo "default-sample-format = float32le" > $PULSE_CONFIG_DIR/daemon.conf + echo "realtime-scheduling = yes" >> $PULSE_CONFIG_DIR/daemon.conf + echo "rlimit-rttime = -1" >> $PULSE_CONFIG_DIR/daemon.conf + echo "exit-idle-time = -1" >> $PULSE_CONFIG_DIR/daemon.conf else - if (! cat ~/.pulse/daemon.conf | grep "default-sample-format = float32le" > /dev/null); then - sed -i '/default-sample-format = /d' ~/.pulse/daemon.conf - echo "default-sample-format = float32le" >> ~/.pulse/daemon.conf + if (! cat $PULSE_CONFIG_DIR/daemon.conf | grep "default-sample-format = float32le" > /dev/null); then + sed -i '/default-sample-format = /d' $PULSE_CONFIG_DIR/daemon.conf + echo "default-sample-format = float32le" >> $PULSE_CONFIG_DIR/daemon.conf fi - if (! cat ~/.pulse/daemon.conf | grep "realtime-scheduling = yes" > /dev/null); then - sed -i '/realtime-scheduling = /d' ~/.pulse/daemon.conf - echo "realtime-scheduling = yes" >> ~/.pulse/daemon.conf + if (! cat $PULSE_CONFIG_DIR/daemon.conf | grep "realtime-scheduling = yes" > /dev/null); then + sed -i '/realtime-scheduling = /d' $PULSE_CONFIG_DIR/daemon.conf + echo "realtime-scheduling = yes" >> $PULSE_CONFIG_DIR/daemon.conf fi - if (! cat ~/.pulse/daemon.conf | grep "rlimit-rttime = -1" > /dev/null); then - sed -i '/rlimit-rttime =/d' ~/.pulse/daemon.conf - echo "rlimit-rttime = -1" >> ~/.pulse/daemon.conf + if (! cat $PULSE_CONFIG_DIR/daemon.conf | grep "rlimit-rttime = -1" > /dev/null); then + sed -i '/rlimit-rttime =/d' $PULSE_CONFIG_DIR/daemon.conf + echo "rlimit-rttime = -1" >> $PULSE_CONFIG_DIR/daemon.conf fi - if (! cat ~/.pulse/daemon.conf | grep "exit-idle-time = -1" > /dev/null); then - sed -i '/exit-idle-time =/d' ~/.pulse/daemon.conf - echo "exit-idle-time = -1" >> ~/.pulse/daemon.conf + if (! cat $PULSE_CONFIG_DIR/daemon.conf | grep "exit-idle-time = -1" > /dev/null); then + sed -i '/exit-idle-time =/d' $PULSE_CONFIG_DIR/daemon.conf + echo "exit-idle-time = -1" >> $PULSE_CONFIG_DIR/daemon.conf fi fi @@ -56,7 +60,7 @@ echo "usage: $0 [command] --dummy Don't do anything, just create the needed files NOTE: - When runned with no arguments, pulse2jack will + When ran with no arguments, pulse2jack will activate PulseAudio with both playback and record modes. " exit @@ -68,18 +72,56 @@ exit -p|--p|--play) PLAY_ONLY="yes" -FILE=$INSTALL_PREFIX/share/cadence/pulse2jack/play.pa ;; *) -FILE=$INSTALL_PREFIX/share/cadence/pulse2jack/play+rec.pa ;; esac +TEMPLATE_PA_FILE=$INSTALL_PREFIX/share/cadence/pulse2jack/template.pa + # ---------------------------------------------- -IsPulseAudioRunning() -{ +addJackConnectionsToPAFile() { + PAFILE=$1 + OUTFILE=$2 + cp $PAFILE $OUTFILE + tac $JACK_CONNFILE | while IFS=\| read name type channels connect; do + sed -i "/### Load Jack modules/a load-module module-jack-$type channels=$channels connect=$connect client_name=\"$name\"" $OUTFILE + done +} + +loadConnectionsIntoPA() { + CONNTYPE=$1 + while IFS=\| read name type channels connect; do + if [ $CONNTYPE == "$type" ] ; then + pactl load-module module-jack-$type channels=$channels connect=$connect client_name="$name" > /dev/null + fi + done < $JACK_CONNFILE +} + +addDefaultSink() { + INFILE=$1 + sed -i "/### Make Jack default/a set-default-sink jack_out" $INFILE +} + +addDefaultSource() { + INFILE=$1 + sed -i "/### Make Jack default/a set-default-source jack_in" $INFILE +} + +if [ ! -f $PULSE_CONFIG_DIR/jack-connections ] ; then + # safety in case there's no config generated yet from GUI + sed "/### Load Jack modules/a load-module module-jack-sink + /### Load Jack modules/a load-module module-jack-source" $TEMPLATE_PA_FILE > $PA_CTLFILE +else + addJackConnectionsToPAFile $TEMPLATE_PA_FILE $PA_CTLFILE +fi + +addDefaultSource $PA_CTLFILE +addDefaultSink $PA_CTLFILE + +IsPulseAudioRunning() { PROCESS=`ps -u $USER | grep pulseaudio` if [ "$PROCESS" == "" ]; then false @@ -89,39 +131,29 @@ IsPulseAudioRunning() } if (IsPulseAudioRunning); then -{ - if (`jack_lsp | grep "PulseAudio JACK Sink:" > /dev/null`); then - { + # get the first sink name from the table + FIRST_SINK_NAME=$(grep '|sink|' $JACK_CONNFILE | head -1 | cut -d\| -f1) + if ($(jack_lsp 2>/dev/null | grep "$FIRST_SINK_NAME" > /dev/null)); then echo "PulseAudio is already running and bridged to JACK" - } else - { echo "PulseAudio is already running, bridge it..." if [ "$PLAY_ONLY" == "yes" ]; then - { - pactl load-module module-jack-sink > /dev/null + loadConnectionsIntoPA "sink" pacmd set-default-source jack_in > /dev/null - } else - { - pactl load-module module-jack-sink > /dev/null - pactl load-module module-jack-source > /dev/null + loadConnectionsIntoPA "source" + loadConnectionsIntoPA "sink" pacmd set-default-sink jack_out > /dev/null pacmd set-default-source jack_in > /dev/null - } fi echo "Done" - } fi -} else -{ - if (`pulseaudio --daemonize --high-priority --realtime --exit-idle-time=-1 --file=$FILE -n`); then + if ($(pulseaudio --daemonize --high-priority --realtime --exit-idle-time=-1 --file=$PA_CTLFILE -n)); then echo "Initiated PulseAudio successfully!" else echo "Failed to initialize PulseAudio!" fi -} fi diff --git a/data/pulse2jack/play.pa b/data/pulse2jack/template.pa old mode 100644 new mode 100755 similarity index 96% rename from data/pulse2jack/play.pa rename to data/pulse2jack/template.pa index 3fc04dd6..4c6a1d5a --- a/data/pulse2jack/play.pa +++ b/data/pulse2jack/template.pa @@ -28,7 +28,6 @@ load-module module-stream-restore load-module module-card-restore ### Load Jack modules -load-module module-jack-sink ### Load unix protocol load-module module-native-protocol-unix @@ -47,4 +46,4 @@ load-module module-rescue-streams load-module module-always-sink ### Make Jack default -set-default-sink jack_out + diff --git a/resources/ui/cadence.ui b/resources/ui/cadence.ui index 267f04d2..2932804e 100644 --- a/resources/ui/cadence.ui +++ b/resources/ui/cadence.ui @@ -6,8 +6,8 @@ 0 0 - 740 - 564 + 822 + 659 @@ -240,770 +240,1019 @@ - - - JACK Status + + + 1 - - - - - - - - - - - :/16x16/dialog-ok-apply.png - - - - - - - - 0 - 0 - - - - Sample Rate: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - DSP Load: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - TextLabel - - - - - - - - 0 - 0 - - - - Realtime: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - :/16x16/dialog-ok-apply.png - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - Buffer Size: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - TextLabel - - - - - - - - 0 - 0 - - - - Xruns: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - TextLabel - - - - - - - - 0 - 0 - - - - Server Status: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - Block Latency: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - TextLabel - - - - - - - TextLabel - - - - - - - TextLabel - - - - - - - TextLabel - - - - - - - - - - 0 - - - 1 - - - Qt::Horizontal - - - - - - - - - Start - - - - - - - Stop - - - - - - - Force Restart - - - - - - - Configure - - - - - - - Switch Master - - - - - - - Qt::Horizontal - - - QSizePolicy::Preferred - - - - 38 - 20 - - - - - - - - Auto-start JACK or LADISH at login - - - - - - - ... - - - - - - - - - - - - - 0 - 0 - - - - JACK Bridges - - - - 2 - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Sunken - - - 0 - - - 1 - - - - - 0 - 0 - 360 - 100 - + + + Jack Status + + + + + + - - ALSA Audio - - - - 2 - - - 0 - - - 2 - - - 2 - - - - - No bridge in use - - - Qt::AlignCenter - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - - - Start - - - - - - - Stop - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - + - - - - - Bridge Type: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - + + + + + + + + + :/16x16/dialog-ok-apply.png + + + + + + + + 0 + 0 + + + + Sample Rate: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + DSP Load: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + 0 + 0 + + - (None) + Realtime: - - + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + - ALSA -> Loop -> JACK + + + + :/16x16/dialog-ok-apply.png + + + Qt::AlignCenter + + + + + + + + 0 + 0 + - - - ALSA -> JACK (Plugin) + Buffer Size: - - + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + - ALSA -> PulseAudio -> JACK (Plugin) + TextLabel - - - - - - - ... - - - - + + + + + + + 0 + 0 + + + + Xruns: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + 0 + 0 + + + + Server Status: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Block Latency: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + TextLabel + + + + + - - - Qt::Vertical - - - QSizePolicy::Preferred + + + 0 - - - 20 - 20 - - - - - - - - - - 0 - 0 - 360 - 97 - - - - ALSA MIDI - - - - 2 - - - 0 - - - 2 - - - 2 - - - - - ALSA MIDI State + + 1 - - Qt::AlignCenter + + Qt::Horizontal - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - + + + Start - - + + Stop - - - - Qt::Horizontal + + + + Force Restart - - QSizePolicy::Fixed + + + + + + Configure - - - 10 - 20 - + + + + + + Switch Master - + - - - - - - + + Qt::Horizontal + + QSizePolicy::Preferred + - 40 + 38 20 - - + + - Export hardware ports + Auto-start JACK or LADISH at login - - + + - Start with Jack + ... - - - - Qt::Vertical - - - QSizePolicy::Preferred - - - - 20 - 20 - - - - - - - - 0 - 0 - 360 - 97 - + + + + + Qt::Vertical - - PulseAudio - - + + + 20 + 40 + + + + + + + + + Jack Bridges + + + + + + + 0 + 0 + + + + + + + + + - 2 + 4 - 0 + 4 - 2 + 4 2 - - - - PulseAudio state + + + + + 0 + 0 + - - Qt::AlignCenter + + QFrame::StyledPanel - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - - - Start + + QFrame::Sunken + + + 0 + + + 1 + + + 1 + + + 0 + + + + + 0 + 0 + 458 + 424 + + + + ALSA + + + + 4 - - - - - - Stop + + 4 - - - - - - Qt::Horizontal + + 2 - - QSizePolicy::Fixed + + 4 - - - 10 - 20 - + + 4 - - - - - - - - - - Qt::Horizontal + + + + 4 + + + 4 + + + 6 + + + + + ALSA Audio + + + + + + + No bridge in use + + + Qt::AlignCenter + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + Start + + + + + + + Stop + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + + Bridge Type: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + (None) + + + + + ALSA -> Loop -> JACK + + + + + ALSA -> JACK (Plugin) + + + + + ALSA -> PulseAudio -> JACK (Plugin) + + + + + + + + ... + + + + + + + + + + + 4 + + + 4 + + + 6 + + + + + Qt::Horizontal + + + + + + + ALSA MIDI + + + + + + + ALSA MIDI State + + + Qt::AlignCenter + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + Start + + + + + + + Stop + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Export hardware ports + + + + + + + Start with Jack + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 20 + 140 + + + + + + + + + + 0 + 0 + 458 + 424 + + + + PulseAudio + + + + 2 - - - 40 - 20 - + + 0 - - - - - - Auto-start at login + + 2 - - - - - - ... + + 2 - - - - - - - - Qt::Vertical - - - QSizePolicy::Preferred - - - - 20 - 20 - - - + + + + PulseAudio state + + + Qt::AlignCenter + + + + + + + 4 + + + 4 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + Start + + + + + + + Stop + + + + + + + Auto-start at login + + + + + + + ... + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 5 + + + + + + + + Qt::Horizontal + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 5 + + + + + + + + 4 + + + + + PulseAudio Sinks and Sources + + + Qt::AlignCenter + + + + + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + 4 + + + true + + + true + + + false + + + true + + + + + + + + + + + 6 + + + 0 + + + 4 + + + 4 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 50 + 50 + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 50 + 50 + + + + - + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 160 + 20 + + + + + + + + false + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Undo + + + + + + + false + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Save + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Defaults + + + + + + + + + + - - - + + + - - - - Qt::Vertical - - - QSizePolicy::Preferred - - - - 20 - 40 - - - - @@ -1572,7 +1821,7 @@ Audio Plugins PATH - AlignHCenter|AlignVCenter|AlignCenter + AlignCenter ItemIsSelectable|ItemIsEnabled @@ -1583,7 +1832,7 @@ Default Applications - AlignHCenter|AlignVCenter|AlignCenter + AlignCenter ItemIsSelectable|ItemIsEnabled @@ -1594,7 +1843,7 @@ WineASIO - AlignHCenter|AlignVCenter|AlignCenter + AlignCenter ItemIsSelectable|ItemIsEnabled @@ -1613,7 +1862,16 @@ - + + 0 + + + 0 + + + 0 + + 0 @@ -1681,8 +1939,8 @@ 0 0 - 416 - 334 + 490 + 385 @@ -1711,8 +1969,8 @@ 0 0 - 94 - 66 + 86 + 72 @@ -1741,8 +1999,8 @@ 0 0 - 94 - 66 + 86 + 72 @@ -1771,8 +2029,8 @@ 0 0 - 94 - 66 + 86 + 72 @@ -1832,12 +2090,21 @@ - - 20 + + 0 + + + 0 + + + 0 - + 0 + + 20 + @@ -2063,7 +2330,16 @@ - + + 0 + + + 0 + + + 0 + + 0 @@ -2297,6 +2573,11 @@ Default is off QLabel
clickablelabel.h
+ + BridgeSourceSink + QTableWidget +
bridgesourcesink.h
+
diff --git a/src/bridgesourcesink.py b/src/bridgesourcesink.py new file mode 100755 index 00000000..c61e5cab --- /dev/null +++ b/src/bridgesourcesink.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Custom QTableWidget that handles pulseaudio source and sinks +# Copyright (C) 2011-2018 Filipe Coelho +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# For a full copy of the GNU General Public License see the COPYING file + +# --------------------------------------------------------------------- +# Imports (Global) + +from collections import namedtuple + +from PyQt5.QtCore import Qt, QRegExp, pyqtSignal +from PyQt5.QtGui import QRegExpValidator +from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView, QComboBox, QLineEdit, QSpinBox, QPushButton, QCheckBox, QHBoxLayout, QWidget +from shared import * +from shared_cadence import GlobalSettings + +# Python3/4 function name normalisation +try: + range = xrange +except NameError: + pass + +PULSE_USER_CONFIG_DIR = os.getenv("PULSE_USER_CONFIG_DIR") +if not PULSE_USER_CONFIG_DIR: + PULSE_USER_CONFIG_DIR = os.path.join(HOME, ".pulse") + +if not os.path.exists(PULSE_USER_CONFIG_DIR): + os.path.mkdir(PULSE_USER_CONFIG_DIR) + +# a data class to hold the Sink/Source Data. Use strings in tuple for easy map(_make) +# but convert to type in table for editor +SSData = namedtuple('SSData', 'name s_type channels connected') + + +# --------------------------------------------------------------------- +# Extend QTableWidget to hold Sink/Source data + +class BridgeSourceSink(QTableWidget): + defaultPASourceData = SSData( + name="PulseAudio JACK Source", + s_type="source", + channels="2", + connected="True") + + defaultPASinkData = SSData( + name="PulseAudio JACK Sink", + s_type="sink", + channels="2", + connected="True") + + customChanged = pyqtSignal() + + def __init__(self, parent): + QTableWidget.__init__(self, parent) + self.bridgeData = [] + if not GlobalSettings.contains("Pulse2JACK/PABridges"): + self.initialise_settings() + self.load_from_settings() + + def load_data_into_cells(self): + self.setHorizontalHeaderLabels(['Name', 'Type', 'Channels', 'Conn?']) + self.setRowCount(0) + + for data in self.bridgeData: + row = self.rowCount() + self.insertRow(row) + + # Name + name_col = QLineEdit() + name_col.setText(data.name) + name_col.textChanged.connect(self.customChanged.emit) + rx = QRegExp("[^|]+") + validator = QRegExpValidator(rx, self) + name_col.setValidator(validator) + self.setCellWidget(row, 0, name_col) + + # Type + combo_box = QComboBox() + + microphone_icon = QIcon.fromTheme('audio-input-microphone') + if microphone_icon.isNull(): + microphone_icon = QIcon.fromTheme('microphone') + + loudspeaker_icon = QIcon.fromTheme('audio-volume-high') + if loudspeaker_icon.isNull(): + loudspeaker_icon = QIcon.fromTheme('player-volume') + + combo_box.addItem(microphone_icon, "source") + combo_box.addItem(loudspeaker_icon, "sink") + + combo_box.setCurrentIndex(0 if data.s_type == "source" else 1) + combo_box.currentTextChanged.connect(self.customChanged.emit) + self.setCellWidget(row, 1, combo_box) + + # Channels + chan_col = QSpinBox() + chan_col.setValue(int(data.channels)) + chan_col.setMinimum(1) + chan_col.setAlignment(Qt.AlignCenter) + chan_col.valueChanged.connect(self.customChanged.emit) + self.setCellWidget(row, 2, chan_col) + + # Auto connect? + auto_cb = QCheckBox() + auto_cb.setObjectName("auto_cb") + auto_cb.setCheckState(Qt.Checked if data.connected in ['true', 'True', 'TRUE'] else Qt.Unchecked) + auto_cb.stateChanged.connect(self.customChanged.emit) + widget = QWidget() + h_layout = QHBoxLayout(widget) + h_layout.addWidget(auto_cb) + h_layout.setAlignment(Qt.AlignCenter) + h_layout.setContentsMargins(0, 0, 0, 0) + widget.setLayout(h_layout) + self.setCellWidget(row, 3, widget) + self.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed) + + def defaults(self): + self.bridgeData = [self.defaultPASourceData, self.defaultPASinkData] + self.load_data_into_cells() + self.customChanged.emit() + + def undo(self): + self.load_from_settings() + self.load_data_into_cells() + self.customChanged.emit() + + def initialise_settings(self): + GlobalSettings.setValue( + "Pulse2JACK/PABridges", + self.encode_bridge_data([self.defaultPASourceData, self.defaultPASinkData])) + + def load_from_settings(self): + bridgeDataText = GlobalSettings.value("Pulse2JACK/PABridges") + self.bridgeData = self.decode_bridge_data(bridgeDataText) + + def hasChanges(self)->bool: + bridgeDataText = GlobalSettings.value("Pulse2JACK/PABridges") + saved_data = self.decode_bridge_data(bridgeDataText) + + if self.rowCount() != len(saved_data): + return True + + for row in range(self.rowCount()): + orig_data = saved_data[row] + + name = self.cellWidget(row, 0).text() + if name != orig_data[0]: + return True + + type = self.cellWidget(row, 1).currentText() + if type != orig_data[1]: + return True + + channels = self.cellWidget(row, 2).value() + if channels != int(orig_data[2]): + return True + + auto_cb = self.cellWidget(row, 3).findChild(QCheckBox, "auto_cb") + connected = auto_cb.isChecked() + if connected != bool(orig_data[3]): + return True + + return False + + def hasValidValues(self)->bool: + used_names = [] + + row_count = self.rowCount() + # Prevent save without any bridge + if not row_count: + return False + + for row in range(row_count): + line_edit = self.cellWidget(row, 0) + name = line_edit.text() + + if not name or name in used_names: + # prevent double name entries + return False + + used_names.append(name) + + return True + + def add_row(self): + # first, search in table which bridge exists + # to add the most pertinent new bridge + has_source = False + has_sink = False + + for row in range(self.rowCount()): + cell_widget = self.cellWidget(row, 1) + + group_type = "" + if cell_widget: + group_type = cell_widget.currentText() + + if group_type == "source": + has_source = True + elif group_type == "sink": + has_sink = True + + if has_source and has_sink: + break + + ss_data = SSData(name="", s_type="source", channels="2", connected="False") + if not has_sink: + ss_data = self.defaultPASinkData + elif not has_source: + ss_data = self.defaultPASourceData + + self.bridgeData.append(ss_data) + self.load_data_into_cells() + self.editItem(self.item(self.rowCount() - 1, 0)) + self.customChanged.emit() + + def remove_row(self): + del self.bridgeData[self.currentRow()] + self.load_data_into_cells() + self.customChanged.emit() + + def save_bridges(self): + self.bridgeData = [] + for row in range(0, self.rowCount()): + new_name = self.cellWidget(row, 0).property("text") + new_type = self.cellWidget(row, 1).currentText() + new_channels = self.cellWidget(row, 2).value() + auto_cb = self.cellWidget(row, 3).findChild(QCheckBox, "auto_cb") + new_conn = auto_cb.checkState() == Qt.Checked + + self.bridgeData.append( + SSData(name=new_name, + s_type=new_type, + channels=new_channels, + connected=str(new_conn))) + GlobalSettings.setValue("Pulse2JACK/PABridges", self.encode_bridge_data(self.bridgeData)) + conn_file_path = os.path.join(PULSE_USER_CONFIG_DIR, "jack-connections") + conn_file = open(conn_file_path, "w") + conn_file.write("\n".join(self.encode_bridge_data(self.bridgeData))) + # Need an extra line at the end + conn_file.write("\n") + conn_file.close() + self.customChanged.emit() + + # encode and decode from tuple so it isn't stored in the settings file as a type, and thus the + # configuration is backwards compatible with versions that don't understand SSData types. + # Uses PIPE symbol as separator + def encode_bridge_data(self, data): + return list(map(lambda s: s.name + "|" + s.s_type + "|" + str(s.channels) + "|" + str(s.connected), data)) + + def decode_bridge_data(self, data): + return list(map(lambda d: SSData._make(d.split("|")), data)) + + def resizeEvent(self, event): + self.setColumnWidth(0, int(self.width() * 0.49)) + self.setColumnWidth(1, int(self.width() * 0.17)) + self.setColumnWidth(2, int(self.width() * 0.17)) + self.setColumnWidth(3, int(self.width() * 0.17)) diff --git a/src/cadence.py b/src/cadence.py index 8b5192f7..369c7462 100755 --- a/src/cadence.py +++ b/src/cadence.py @@ -23,10 +23,10 @@ if True: from PyQt5.QtCore import QFileSystemWatcher, QThread, QSemaphore - from PyQt5.QtWidgets import QApplication, QDialogButtonBox, QLabel, QMainWindow, QSizePolicy + from PyQt5.QtWidgets import QApplication, QDialogButtonBox, QLabel, QMainWindow #, QSizePolicy, QTableWidget, QTableWidgetItem, QHeaderView else: from PyQt4.QtCore import QFileSystemWatcher, QThread, QSemaphore - from PyQt4.QtGui import QApplication, QDialogButtonBox, QLabel, QMainWindow, QSizePolicy + from PyQt4.QtGui import QApplication, QDialogButtonBox, QLabel, QMainWindow # , QSizePolicy, QTableWidget, QTableWidgetItem, QHeaderView # ------------------------------------------------------------------------------------------------------------ # Imports (Custom Stuff) @@ -982,6 +982,8 @@ def __init__(self, parent=None): if self.cb_app_browser.count() == 0: self.ch_app_browser.setEnabled(False) + self.tableSinkSourceData.load_data_into_cells() + mimeappsPath = os.path.join(HOME, ".local", "share", "applications", "mimeapps.list") if os.path.exists(mimeappsPath): @@ -1132,6 +1134,14 @@ def __init__(self, parent=None): self.b_pulse_stop.clicked.connect(self.slot_PulseAudioBridgeStop) self.tb_pulse_options.clicked.connect(self.slot_PulseAudioBridgeOptions) + self.b_bridge_add.clicked.connect(self.slot_PulseAudioBridgeAdd) + self.b_bridge_remove.clicked.connect(self.slot_PulseAudioBridgeRemove) + self.b_bridge_undo.clicked.connect(self.slot_PulseAudioBridgeUndo) + self.b_bridge_save.clicked.connect(self.slot_PulseAudioBridgeSave) + self.b_bridge_defaults.clicked.connect(self.slot_PulseAudioBridgeDefaults) + self.tableSinkSourceData.customChanged.connect(self.slot_PulseAudioBridgeTableChanged) + self.tableSinkSourceData.doubleClicked.connect(self.slot_PulseAudioBridgeTableChanged) + self.pic_catia.clicked.connect(self.func_start_catia) self.pic_claudia.clicked.connect(self.func_start_claudia) self.pic_meter_in.clicked.connect(self.func_start_jackmeter_in) @@ -1226,11 +1236,13 @@ def DBusReconnect(self): except: version, groups, conns = (list(), list(), list()) + pa_first_group_name = self.getFirstPulseAudioGroupName() + for group_id, group_name, ports in groups: if group_name == "alsa2jack": global jackClientIdALSA jackClientIdALSA = group_id - elif group_name == "PulseAudio JACK Sink": + elif group_name in (pa_first_group_name, "PulseAudio JACK Sink"): global jackClientIdPulse jackClientIdPulse = group_id @@ -1405,6 +1417,14 @@ def a2jStopped(self): self.systray.setActionEnabled("a2j_stop", False) self.label_bridge_a2j.setText(self.tr("ALSA MIDI Bridge is stopped")) + def getFirstPulseAudioGroupName(self)->str: + # search PulseAudio JACK first group in settings + pa_bridges_settings = GlobalSettings.value("Pulse2JACK/PABridges", type=list) + if pa_bridges_settings: + return pa_bridges_settings[0].partition('|')[0] + + return "PulseAudio JACK Sink" + def checkAlsaAudio(self): asoundrcFile = os.path.join(HOME, ".asoundrc") @@ -1616,11 +1636,13 @@ def slot_DBusJackServerStoppedCallback(self): @pyqtSlot(int, str) def slot_DBusJackClientAppearedCallback(self, group_id, group_name): + pa_first_group_name = self.getFirstPulseAudioGroupName() + if group_name == "alsa2jack": global jackClientIdALSA jackClientIdALSA = group_id self.checkAlsaAudio() - elif group_name == "PulseAudio JACK Sink": + elif group_name in ("PulseAudio JACK Sink", pa_first_group_name): global jackClientIdPulse jackClientIdPulse = group_id self.checkPulseAudio() @@ -1797,6 +1819,33 @@ def slot_PulseAudioBridgeStart(self): def slot_PulseAudioBridgeStop(self): os.system("pulseaudio -k") + @pyqtSlot() + def slot_PulseAudioBridgeTableChanged(self): + has_changes = self.tableSinkSourceData.hasChanges() + has_valid_values = self.tableSinkSourceData.hasValidValues() + self.b_bridge_save.setEnabled(has_changes and has_valid_values) + self.b_bridge_undo.setEnabled(has_changes) + + @pyqtSlot() + def slot_PulseAudioBridgeAdd(self): + self.tableSinkSourceData.add_row() + + @pyqtSlot() + def slot_PulseAudioBridgeRemove(self): + self.tableSinkSourceData.remove_row() + + @pyqtSlot() + def slot_PulseAudioBridgeUndo(self): + self.tableSinkSourceData.undo() + + @pyqtSlot() + def slot_PulseAudioBridgeSave(self): + self.tableSinkSourceData.save_bridges() + + @pyqtSlot() + def slot_PulseAudioBridgeDefaults(self): + self.tableSinkSourceData.defaults() + @pyqtSlot() def slot_PulseAudioBridgeOptions(self): ToolBarPADialog(self).exec_() diff --git a/src/claudia_launcher.py b/src/claudia_launcher.py index 64af08e3..77d0e145 100755 --- a/src/claudia_launcher.py +++ b/src/claudia_launcher.py @@ -168,7 +168,7 @@ def __init__(self, parent): self.clearInfo_DAW() self.clearInfo_Host() - self.clearInfo_Intrument() + self.clearInfo_Instrument() self.clearInfo_Bristol() self.clearInfo_Plugin() self.clearInfo_Effect() @@ -547,7 +547,7 @@ def clearInfo_Host(self): self.frame_Host.setEnabled(False) self.showDoc_Host("", "") - def clearInfo_Intrument(self): + def clearInfo_Instrument(self): self.ico_app_ins.setPixmap(self.getIcon("start-here").pixmap(48, 48)) self.label_name_ins.setText("App Name") self.ico_builtin_fx_ins.setPixmap(self.getIconForYesNo(False).pixmap(16, 16)) @@ -983,7 +983,7 @@ def slot_checkSelectedInstrument(self, row): Docs0 = Docs[0] if (os.path.exists(Docs[0].replace("file://", ""))) else "" self.showDoc_Instrument(Docs0, Docs[1]) else: - self.clearInfo_Intrument() + self.clearInfo_Instrument() self.callback_checkGUI(row >= 0)