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.

272 lines
9.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, pyqtSignal
  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. customChanged = pyqtSignal()
  52. def __init__(self, parent):
  53. QTableWidget.__init__(self, parent)
  54. self.bridgeData = []
  55. if not GlobalSettings.contains("Pulse2JACK/PABridges"):
  56. self.initialise_settings()
  57. self.load_from_settings()
  58. def load_data_into_cells(self):
  59. self.setHorizontalHeaderLabels(['Name', 'Type', 'Channels', 'Conn?'])
  60. self.setRowCount(0)
  61. for data in self.bridgeData:
  62. row = self.rowCount()
  63. self.insertRow(row)
  64. # Name
  65. name_col = QLineEdit()
  66. name_col.setText(data.name)
  67. name_col.textChanged.connect(self.customChanged.emit)
  68. rx = QRegExp("[^|]+")
  69. validator = QRegExpValidator(rx, self)
  70. name_col.setValidator(validator)
  71. self.setCellWidget(row, 0, name_col)
  72. # Type
  73. combo_box = QComboBox()
  74. microphone_icon = QIcon.fromTheme('audio-input-microphone')
  75. if microphone_icon.isNull():
  76. microphone_icon = QIcon.fromTheme('microphone')
  77. loudspeaker_icon = QIcon.fromTheme('audio-volume-high')
  78. if loudspeaker_icon.isNull():
  79. loudspeaker_icon = QIcon.fromTheme('player-volume')
  80. combo_box.addItem(microphone_icon, "source")
  81. combo_box.addItem(loudspeaker_icon, "sink")
  82. combo_box.setCurrentIndex(0 if data.s_type == "source" else 1)
  83. combo_box.currentTextChanged.connect(self.customChanged.emit)
  84. self.setCellWidget(row, 1, combo_box)
  85. # Channels
  86. chan_col = QSpinBox()
  87. chan_col.setValue(int(data.channels))
  88. chan_col.setMinimum(1)
  89. chan_col.setAlignment(Qt.AlignCenter)
  90. chan_col.valueChanged.connect(self.customChanged.emit)
  91. self.setCellWidget(row, 2, chan_col)
  92. # Auto connect?
  93. auto_cb = QCheckBox()
  94. auto_cb.setObjectName("auto_cb")
  95. auto_cb.setCheckState(Qt.Checked if data.connected in ['true', 'True', 'TRUE'] else Qt.Unchecked)
  96. auto_cb.stateChanged.connect(self.customChanged.emit)
  97. widget = QWidget()
  98. h_layout = QHBoxLayout(widget)
  99. h_layout.addWidget(auto_cb)
  100. h_layout.setAlignment(Qt.AlignCenter)
  101. h_layout.setContentsMargins(0, 0, 0, 0)
  102. widget.setLayout(h_layout)
  103. self.setCellWidget(row, 3, widget)
  104. self.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed)
  105. def defaults(self):
  106. self.bridgeData = [self.defaultPASourceData, self.defaultPASinkData]
  107. self.load_data_into_cells()
  108. self.customChanged.emit()
  109. def undo(self):
  110. self.load_from_settings()
  111. self.load_data_into_cells()
  112. self.customChanged.emit()
  113. def initialise_settings(self):
  114. GlobalSettings.setValue(
  115. "Pulse2JACK/PABridges",
  116. self.encode_bridge_data([self.defaultPASourceData, self.defaultPASinkData]))
  117. def load_from_settings(self):
  118. bridgeDataText = GlobalSettings.value("Pulse2JACK/PABridges")
  119. self.bridgeData = self.decode_bridge_data(bridgeDataText)
  120. def hasChanges(self)->bool:
  121. bridgeDataText = GlobalSettings.value("Pulse2JACK/PABridges")
  122. saved_data = self.decode_bridge_data(bridgeDataText)
  123. if self.rowCount() != len(saved_data):
  124. return True
  125. for row in range(self.rowCount()):
  126. orig_data = saved_data[row]
  127. name = self.cellWidget(row, 0).text()
  128. if name != orig_data[0]:
  129. return True
  130. type = self.cellWidget(row, 1).currentText()
  131. if type != orig_data[1]:
  132. return True
  133. channels = self.cellWidget(row, 2).value()
  134. if channels != int(orig_data[2]):
  135. return True
  136. auto_cb = self.cellWidget(row, 3).findChild(QCheckBox, "auto_cb")
  137. connected = auto_cb.isChecked()
  138. if connected != bool(orig_data[3]):
  139. return True
  140. return False
  141. def hasValidValues(self)->bool:
  142. used_names = []
  143. row_count = self.rowCount()
  144. # Prevent save without any bridge
  145. if not row_count:
  146. return False
  147. for row in range(row_count):
  148. line_edit = self.cellWidget(row, 0)
  149. name = line_edit.text()
  150. if not name or name in used_names:
  151. # prevent double name entries
  152. return False
  153. used_names.append(name)
  154. return True
  155. def add_row(self):
  156. # first, search in table which bridge exists
  157. # to add the most pertinent new bridge
  158. has_source = False
  159. has_sink = False
  160. for row in range(self.rowCount()):
  161. cell_widget = self.cellWidget(row, 1)
  162. group_type = ""
  163. if cell_widget:
  164. group_type = cell_widget.currentText()
  165. if group_type == "source":
  166. has_source = True
  167. elif group_type == "sink":
  168. has_sink = True
  169. if has_source and has_sink:
  170. break
  171. ss_data = SSData(name="", s_type="source", channels="2", connected="False")
  172. if not has_sink:
  173. ss_data = self.defaultPASinkData
  174. elif not has_source:
  175. ss_data = self.defaultPASourceData
  176. self.bridgeData.append(ss_data)
  177. self.load_data_into_cells()
  178. self.editItem(self.item(self.rowCount() - 1, 0))
  179. self.customChanged.emit()
  180. def remove_row(self):
  181. del self.bridgeData[self.currentRow()]
  182. self.load_data_into_cells()
  183. self.customChanged.emit()
  184. def save_bridges(self):
  185. self.bridgeData = []
  186. for row in range(0, self.rowCount()):
  187. new_name = self.cellWidget(row, 0).property("text")
  188. new_type = self.cellWidget(row, 1).currentText()
  189. new_channels = self.cellWidget(row, 2).value()
  190. auto_cb = self.cellWidget(row, 3).findChild(QCheckBox, "auto_cb")
  191. new_conn = auto_cb.checkState() == Qt.Checked
  192. self.bridgeData.append(
  193. SSData(name=new_name,
  194. s_type=new_type,
  195. channels=new_channels,
  196. connected=str(new_conn)))
  197. GlobalSettings.setValue("Pulse2JACK/PABridges", self.encode_bridge_data(self.bridgeData))
  198. conn_file_path = os.path.join(PULSE_USER_CONFIG_DIR, "jack-connections")
  199. conn_file = open(conn_file_path, "w")
  200. conn_file.write("\n".join(self.encode_bridge_data(self.bridgeData)))
  201. # Need an extra line at the end
  202. conn_file.write("\n")
  203. conn_file.close()
  204. self.customChanged.emit()
  205. # encode and decode from tuple so it isn't stored in the settings file as a type, and thus the
  206. # configuration is backwards compatible with versions that don't understand SSData types.
  207. # Uses PIPE symbol as separator
  208. def encode_bridge_data(self, data):
  209. return list(map(lambda s: s.name + "|" + s.s_type + "|" + str(s.channels) + "|" + str(s.connected), data))
  210. def decode_bridge_data(self, data):
  211. return list(map(lambda d: SSData._make(d.split("|")), data))
  212. def resizeEvent(self, event):
  213. self.setColumnWidth(0, int(self.width() * 0.49))
  214. self.setColumnWidth(1, int(self.width() * 0.17))
  215. self.setColumnWidth(2, int(self.width() * 0.17))
  216. self.setColumnWidth(3, int(self.width() * 0.17))