Browse Source

Allow creation of multiple named Pulse Audio Sink/Sources

pull/290/head
Mark Fisher 4 years ago
parent
commit
3be1b7113d
7 changed files with 1262 additions and 740 deletions
  1. +3
    -0
      .gitignore
  2. +76
    -32
      data/cadence-pulse2jack
  3. +1
    -2
      data/pulse2jack/template.pa
  4. +982
    -701
      resources/ui/cadence.ui
  5. +150
    -0
      src/bridgesourcesink.py
  6. +47
    -2
      src/cadence.py
  7. +3
    -3
      src/claudia_launcher.py

+ 3
- 0
.gitignore View File

@@ -42,3 +42,6 @@ src/dist/

# Other
data/templates/energyXT.xt

# IDEs
.idea/

+ 76
- 32
data/cadence-pulse2jack View File

@@ -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,16 +72,55 @@ 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

# ----------------------------------------------

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`
@@ -90,6 +133,7 @@ IsPulseAudioRunning()

if (IsPulseAudioRunning); then
{

if (`jack_lsp | grep "PulseAudio JACK Sink:" > /dev/null`); then
{
echo "PulseAudio is already running and bridged to JACK"
@@ -100,13 +144,13 @@ if (IsPulseAudioRunning); then

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
}
@@ -118,7 +162,7 @@ if (IsPulseAudioRunning); then
}
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!"


data/pulse2jack/play.pa → data/pulse2jack/template.pa View File

@@ -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

+ 982
- 701
resources/ui/cadence.ui
File diff suppressed because it is too large
View File


+ 150
- 0
src/bridgesourcesink.py View File

@@ -0,0 +1,150 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Custom QTableWidget that handles pulseaudio source and sinks
# Copyright (C) 2011-2018 Filipe Coelho <falktx@falktx.com>
#
# 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
from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView, QComboBox
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")

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', 'Auto Connect'])
self.setRowCount(0)
for data in self.bridgeData:
row = self.rowCount()
self.insertRow(row)
# convert to types from strings so the default editors can be used
self.setItem(row, 0, self.create_widget_item(data.name))
self.setItem(row, 1, self.create_widget_item(data.s_type))
# self.setCellWidget(row, 1, self.create_combo_item(data.s_type))
self.setItem(row, 2, self.create_widget_item(int(data.channels)))
self.setItem(row, 3, self.create_widget_item(data.connected in ['true', 'True', 'TRUE']))
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)

def create_widget_item(self, i):
item = QTableWidgetItem()
item.setData(Qt.EditRole, i)
return item

def create_combo_item(self, v):
comboBox = QComboBox()
comboBox.addItems(["source", "sink"])
comboBox.setCurrentIndex(0 if v == "source" else 1)
return comboBox

def defaults(self):
self.bridgeData = [self.defaultPASourceData, self.defaultPASinkData]
self.load_data_into_cells()

def undo(self):
self.load_from_settings()
self.load_data_into_cells()

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 add_row(self):
self.bridgeData.append(SSData(name="", s_type="source", channels="2", connected="false"))
self.load_data_into_cells()
self.editItem(self.item(self.rowCount() - 1, 0))

def remove_row(self):
currentRow = self.currentRow()
del self.bridgeData[currentRow]
self.load_data_into_cells()

def save_bridges(self):
self.bridgeData = []
for row in range(0, self.rowCount()):
new_name = self.item(row, 0).text()
new_type = self.item(row, 1).text()
new_channels = self.item(row, 2).text()
new_conn = self.item(row, 3).text()
self.bridgeData.append(
SSData(name=new_name,
s_type=new_type,
channels=new_channels,
connected=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()

# 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
# TODO: fail if any pipes in names as it will break the encode/decode of the data
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))

+ 47
- 2
src/cadence.py View File

@@ -23,10 +23,10 @@ from platform import architecture

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 @@ class CadenceMainW(QMainWindow, ui_cadence.Ui_CadenceMainW):
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 @@ class CadenceMainW(QMainWindow, ui_cadence.Ui_CadenceMainW):
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.itemChanged.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)
@@ -1797,6 +1807,41 @@ class CadenceMainW(QMainWindow, ui_cadence.Ui_CadenceMainW):
def slot_PulseAudioBridgeStop(self):
os.system("pulseaudio -k")

@pyqtSlot()
def slot_PulseAudioBridgeTableChanged(self):
self.b_bridge_save.setEnabled(True)
self.b_bridge_undo.setEnabled(True)

@pyqtSlot()
def slot_PulseAudioBridgeAdd(self):
self.tableSinkSourceData.add_row()
self.b_bridge_save.setEnabled(True)
self.b_bridge_undo.setEnabled(True)

@pyqtSlot()
def slot_PulseAudioBridgeRemove(self):
self.tableSinkSourceData.remove_row()
self.b_bridge_save.setEnabled(True)
self.b_bridge_undo.setEnabled(True)

@pyqtSlot()
def slot_PulseAudioBridgeUndo(self):
self.tableSinkSourceData.undo()
self.b_bridge_save.setEnabled(False)
self.b_bridge_undo.setEnabled(False)

@pyqtSlot()
def slot_PulseAudioBridgeSave(self):
self.tableSinkSourceData.save_bridges()
self.b_bridge_save.setEnabled(False)
self.b_bridge_undo.setEnabled(False)

@pyqtSlot()
def slot_PulseAudioBridgeDefaults(self):
self.tableSinkSourceData.defaults()
self.b_bridge_save.setEnabled(True)
self.b_bridge_undo.setEnabled(True)

@pyqtSlot()
def slot_PulseAudioBridgeOptions(self):
ToolBarPADialog(self).exec_()


+ 3
- 3
src/claudia_launcher.py View File

@@ -168,7 +168,7 @@ class ClaudiaLauncher(QWidget, ui_claudia_launcher.Ui_ClaudiaLauncherW):

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 @@ class ClaudiaLauncher(QWidget, ui_claudia_launcher.Ui_ClaudiaLauncherW):
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 @@ class ClaudiaLauncher(QWidget, ui_claudia_launcher.Ui_ClaudiaLauncherW):
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)



Loading…
Cancel
Save