Collection of tools useful for audio production
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

195 lines
7.5KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Custom QTableWidget that handles pulseaudio source and sinks
  4. # Copyright (C) 2011-2018 Filipe Coelho <falktx@falktx.com>
  5. #
  6. # This program is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 2 of the License, or
  9. # any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # For a full copy of the GNU General Public License see the COPYING file
  17. # ---------------------------------------------------------------------
  18. # Imports (Global)
  19. from collections import namedtuple
  20. from PyQt5.QtCore import Qt, QRegExp
  21. from PyQt5.QtGui import QRegExpValidator
  22. from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView, QComboBox, QLineEdit, QSpinBox, QPushButton, QCheckBox, QHBoxLayout, QWidget
  23. from shared import *
  24. from shared_cadence import GlobalSettings
  25. # Python3/4 function name normalisation
  26. try:
  27. range = xrange
  28. except NameError:
  29. pass
  30. PULSE_USER_CONFIG_DIR = os.getenv("PULSE_USER_CONFIG_DIR")
  31. if not PULSE_USER_CONFIG_DIR:
  32. PULSE_USER_CONFIG_DIR = os.path.join(HOME, ".pulse")
  33. if not os.path.exists(PULSE_USER_CONFIG_DIR):
  34. os.path.mkdir(PULSE_USER_CONFIG_DIR)
  35. # a data class to hold the Sink/Source Data. Use strings in tuple for easy map(_make)
  36. # but convert to type in table for editor
  37. SSData = namedtuple('SSData', 'name s_type channels connected')
  38. # ---------------------------------------------------------------------
  39. # Extend QTableWidget to hold Sink/Source data
  40. class BridgeSourceSink(QTableWidget):
  41. defaultPASourceData = SSData(
  42. name="PulseAudio JACK Source",
  43. s_type="source",
  44. channels="2",
  45. connected="True")
  46. defaultPASinkData = SSData(
  47. name="PulseAudio JACK Sink",
  48. s_type="sink",
  49. channels="2",
  50. connected="True")
  51. def __init__(self, parent):
  52. QTableWidget.__init__(self, parent)
  53. self.bridgeData = []
  54. if not GlobalSettings.contains("Pulse2JACK/PABridges"):
  55. self.initialise_settings()
  56. self.load_from_settings()
  57. def load_data_into_cells(self):
  58. self.setHorizontalHeaderLabels(['Name', 'Type', 'Channels', 'Conn?'])
  59. self.setRowCount(0)
  60. for data in self.bridgeData:
  61. row = self.rowCount()
  62. self.insertRow(row)
  63. # Name
  64. name_col = QLineEdit()
  65. name_col.setText(data.name)
  66. name_col.returnPressed.connect(self.enable_buttons)
  67. rx = QRegExp("[^|]+")
  68. validator = QRegExpValidator(rx, self)
  69. name_col.setValidator(validator)
  70. self.setCellWidget(row, 0, name_col)
  71. # Type
  72. combo_box = QComboBox()
  73. microphone_icon = QIcon.fromTheme('audio-input-microphone')
  74. if microphone_icon.isNull():
  75. microphone_icon = QIcon.fromTheme('microphone')
  76. loudspeaker_icon = QIcon.fromTheme('audio-volume-high')
  77. if loudspeaker_icon.isNull():
  78. loudspeaker_icon = QIcon.fromTheme('player-volume')
  79. combo_box.addItem(microphone_icon, "source")
  80. combo_box.addItem(loudspeaker_icon, "sink")
  81. combo_box.setCurrentIndex(0 if data.s_type == "source" else 1)
  82. combo_box.currentTextChanged.connect(self.enable_buttons)
  83. self.setCellWidget(row, 1, combo_box)
  84. # Channels
  85. chan_col = QSpinBox()
  86. chan_col.setValue(int(data.channels))
  87. chan_col.setMinimum(1)
  88. chan_col.valueChanged.connect(self.enable_buttons)
  89. self.setCellWidget(row, 2, chan_col)
  90. # Auto connect?
  91. auto_cb = QCheckBox()
  92. auto_cb.setObjectName("auto_cb")
  93. auto_cb.setCheckState(Qt.Checked if data.connected in ['true', 'True', 'TRUE'] else Qt.Unchecked)
  94. auto_cb.stateChanged.connect(self.enable_buttons)
  95. widget = QWidget()
  96. h_layout = QHBoxLayout(widget)
  97. h_layout.addWidget(auto_cb)
  98. h_layout.setAlignment(Qt.AlignCenter)
  99. h_layout.setContentsMargins(0, 0, 0, 0)
  100. widget.setLayout(h_layout)
  101. self.setCellWidget(row, 3, widget)
  102. self.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed)
  103. def enable_buttons(self):
  104. # Can't work out how to tell the table that data has changed (to cause the buttons to become enabled),
  105. # so instead manually make the buttons enabled.
  106. for btn_name in ["b_bridge_save", "b_bridge_undo"]:
  107. self.parent().findChild(QPushButton, btn_name).setProperty("enabled", True)
  108. def defaults(self):
  109. self.bridgeData = [self.defaultPASourceData, self.defaultPASinkData]
  110. self.load_data_into_cells()
  111. def undo(self):
  112. self.load_from_settings()
  113. self.load_data_into_cells()
  114. def initialise_settings(self):
  115. GlobalSettings.setValue(
  116. "Pulse2JACK/PABridges",
  117. self.encode_bridge_data([self.defaultPASourceData, self.defaultPASinkData]))
  118. def load_from_settings(self):
  119. bridgeDataText = GlobalSettings.value("Pulse2JACK/PABridges")
  120. self.bridgeData = self.decode_bridge_data(bridgeDataText)
  121. def add_row(self):
  122. self.bridgeData.append(SSData(name="", s_type="source", channels="2", connected="False"))
  123. self.load_data_into_cells()
  124. self.editItem(self.item(self.rowCount() - 1, 0))
  125. def remove_row(self):
  126. del self.bridgeData[self.currentRow()]
  127. self.load_data_into_cells()
  128. def save_bridges(self):
  129. self.bridgeData = []
  130. for row in range(0, self.rowCount()):
  131. new_name = self.cellWidget(row, 0).property("text")
  132. new_type = self.cellWidget(row, 1).currentText()
  133. new_channels = self.cellWidget(row, 2).value()
  134. auto_cb = self.cellWidget(row, 3).findChild(QCheckBox, "auto_cb")
  135. new_conn = auto_cb.checkState() == Qt.Checked
  136. self.bridgeData.append(
  137. SSData(name=new_name,
  138. s_type=new_type,
  139. channels=new_channels,
  140. connected=str(new_conn)))
  141. GlobalSettings.setValue("Pulse2JACK/PABridges", self.encode_bridge_data(self.bridgeData))
  142. conn_file_path = os.path.join(PULSE_USER_CONFIG_DIR, "jack-connections")
  143. conn_file = open(conn_file_path, "w")
  144. conn_file.write("\n".join(self.encode_bridge_data(self.bridgeData)))
  145. # Need an extra line at the end
  146. conn_file.write("\n")
  147. conn_file.close()
  148. # encode and decode from tuple so it isn't stored in the settings file as a type, and thus the
  149. # configuration is backwards compatible with versions that don't understand SSData types.
  150. # Uses PIPE symbol as separator
  151. def encode_bridge_data(self, data):
  152. return list(map(lambda s: s.name + "|" + s.s_type + "|" + str(s.channels) + "|" + str(s.connected), data))
  153. def decode_bridge_data(self, data):
  154. return list(map(lambda d: SSData._make(d.split("|")), data))
  155. def resizeEvent(self, event):
  156. self.setColumnWidth(0, int(self.width() * 0.49))
  157. self.setColumnWidth(1, int(self.width() * 0.17))
  158. self.setColumnWidth(2, int(self.width() * 0.17))
  159. self.setColumnWidth(3, int(self.width() * 0.17))