├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── qml.qrc ├── qtquickcontrols2.conf ├── resources ├── PipeControl.desktop ├── Settings.qml ├── Top.qml ├── main.qml ├── pipecontrol.svg └── translations │ └── pipecontrol_it_IT.ts └── src ├── main.cpp ├── pw ├── media-session │ ├── alsaproperties.cpp │ └── alsaproperties.h ├── qpipewire.cpp ├── qpipewire.h ├── qpipewirealsanode.cpp ├── qpipewirealsanode.h ├── qpipewireclient.cpp ├── qpipewireclient.h ├── qpipewiredevice.cpp ├── qpipewiredevice.h ├── qpipewirelink.cpp ├── qpipewirelink.h ├── qpipewiremetadata.cpp ├── qpipewiremetadata.h ├── qpipewirenode.cpp ├── qpipewirenode.h ├── qpipewirenodelistmodel.cpp ├── qpipewirenodelistmodel.h ├── qpipewireport.cpp ├── qpipewireport.h ├── qpipewireprofiler.cpp ├── qpipewireprofiler.h ├── qpipewiresettings.cpp ├── qpipewiresettings.h ├── utils.cpp └── utils.h ├── systemdservice.cpp └── systemdservice.h /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | 4 | *~ 5 | *.autosave 6 | *.a 7 | *.core 8 | *.moc 9 | *.o 10 | *.obj 11 | *.orig 12 | *.rej 13 | *.so 14 | *.so.* 15 | *_pch.h.cpp 16 | *_resource.rc 17 | *.qm 18 | .#* 19 | *.*# 20 | core 21 | !core/ 22 | tags 23 | .DS_Store 24 | .directory 25 | *.debug 26 | Makefile* 27 | *.prl 28 | *.app 29 | moc_*.cpp 30 | ui_*.h 31 | qrc_*.cpp 32 | Thumbs.db 33 | *.res 34 | *.rc 35 | /.qmake.cache 36 | /.qmake.stash 37 | /.cache 38 | 39 | # qtcreator generated files 40 | *.pro.user* 41 | 42 | # xemacs temporary files 43 | *.flc 44 | 45 | # Vim temporary files 46 | .*.swp 47 | 48 | # Visual Studio generated files 49 | *.ib_pdb_index 50 | *.idb 51 | *.ilk 52 | *.pdb 53 | *.sln 54 | *.suo 55 | *.vcproj 56 | *vcproj.*.*.user 57 | *.ncb 58 | *.sdf 59 | *.opensdf 60 | *.vcxproj 61 | *vcxproj.* 62 | 63 | # Intellij configuration files 64 | .idea/ 65 | 66 | # MinGW generated files 67 | *.Debug 68 | *.Release 69 | 70 | # Python byte code 71 | *.pyc 72 | 73 | # Binaries 74 | # -------- 75 | *.dll 76 | *.exe 77 | 78 | CMakeLists.txt.user 79 | /build-*/ 80 | /cmake-build-*/ 81 | /build/ 82 | /build-dir/ 83 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | project(pipecontrol VERSION 0.3.0 LANGUAGES C CXX) 4 | 5 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 6 | 7 | set(CMAKE_AUTOUIC ON) 8 | set(CMAKE_AUTOMOC ON) 9 | set(CMAKE_AUTORCC ON) 10 | 11 | # C++ designated initializers only available with ‘-std=c++20’ or ‘-std=gnu++20’ 12 | set(CMAKE_CXX_STANDARD 20) 13 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 14 | 15 | set(KF_MIN_VERSION "6.8.0") 16 | set(QT_MIN_VERSION "6.8.0") 17 | set(QT_VERSION_MAJOR "6") 18 | 19 | find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) 20 | 21 | set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) 22 | 23 | include(KDEInstallDirs) 24 | include(KDECMakeSettings) 25 | include(KDECompilerSettings NO_POLICY_SCOPE) 26 | 27 | find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS Core Qml Quick LinguistTools QuickControls2 DBus) 28 | find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n CoreAddons REQUIRED) 29 | 30 | find_package(PkgConfig) 31 | pkg_check_modules(libpipewire REQUIRED libpipewire-0.3) 32 | 33 | add_definitions( -fexceptions ) 34 | 35 | set(TS_FILES resources/translations/pipecontrol_it_IT.ts) 36 | 37 | set(PROJECT_SOURCES 38 | src/main.cpp 39 | src/pw/qpipewire.h 40 | src/pw/qpipewire.cpp 41 | src/pw/qpipewiremetadata.h 42 | src/pw/qpipewiremetadata.cpp 43 | src/pw/qpipewiresettings.h 44 | src/pw/qpipewiresettings.cpp 45 | src/pw/qpipewireclient.h 46 | src/pw/qpipewireclient.cpp 47 | src/pw/qpipewirenode.h 48 | src/pw/qpipewirenode.cpp 49 | src/pw/qpipewirealsanode.h 50 | src/pw/qpipewirealsanode.cpp 51 | src/pw/qpipewiredevice.h 52 | src/pw/qpipewiredevice.cpp 53 | src/pw/qpipewirelink.h 54 | src/pw/qpipewirelink.cpp 55 | src/pw/qpipewireport.h 56 | src/pw/qpipewireport.cpp 57 | src/pw/qpipewireprofiler.h 58 | src/pw/qpipewireprofiler.cpp 59 | src/pw/qpipewirenodelistmodel.h 60 | src/pw/qpipewirenodelistmodel.cpp 61 | src/pw/media-session/alsaproperties.h 62 | src/pw/media-session/alsaproperties.cpp 63 | src/pw/utils.h 64 | src/pw/utils.cpp 65 | src/systemdservice.h 66 | src/systemdservice.cpp 67 | qml.qrc 68 | ${TS_FILES} 69 | ) 70 | 71 | if("${QT_VERSION_MAJOR}" GREATER_EQUAL 6) 72 | qt_add_executable(pipecontrol 73 | MANUAL_FINALIZATION 74 | ${PROJECT_SOURCES} 75 | ) 76 | qt_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES}) 77 | else() 78 | add_executable(pipecontrol 79 | ${PROJECT_SOURCES} 80 | ) 81 | qt5_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES}) 82 | endif() 83 | 84 | target_compile_definitions(pipecontrol 85 | PRIVATE $<$,$>:QT_QML_DEBUG> 86 | PRIVATE -DPIPECONTROL_VERSION="${CMAKE_PROJECT_VERSION}" 87 | PRIVATE -DKF6_COMPILED_VERSION="${KF6_VERSION}") 88 | target_compile_options(pipecontrol 89 | PRIVATE ${libpipewire_CFLAGS_OTHER}) 90 | target_compile_definitions(pipecontrol PRIVATE 91 | PROJECT_VERSION="${PROJECT_VERSION}" 92 | INSTALL_PREFIX="${CMAKE_INSTALL_PREFIX}") 93 | target_include_directories(pipecontrol SYSTEM 94 | PRIVATE ${libpipewire_INCLUDE_DIRS}) 95 | target_link_libraries(pipecontrol 96 | PRIVATE Qt::Core Qt::Quick Qt::QuickControls2 Qt::DBus 97 | PRIVATE ${libpipewire_LIBRARIES} 98 | PRIVATE KF6::Kirigami KF6::I18n 99 | ) 100 | 101 | configure_file( 102 | ${CMAKE_SOURCE_DIR}/resources/PipeControl.desktop 103 | ${CMAKE_BINARY_DIR}/PipeControl.desktop 104 | ) 105 | 106 | if(QT_VERSION_MAJOR EQUAL 6) 107 | qt_import_qml_plugins(pipecontrol) 108 | qt_finalize_executable(pipecontrol) 109 | endif() 110 | 111 | install(TARGETS pipecontrol RUNTIME DESTINATION "bin/") 112 | install(FILES ${CMAKE_BINARY_DIR}/PipeControl.desktop DESTINATION "share/applications/" ) 113 | install(FILES resources/pipecontrol.svg DESTINATION "share/icons/hicolor/scalable/apps/" ) 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PipeControl 2 | Pipewire control GUI program in Qt-QML using Kirigami2 (KDE) 3 | 4 | Work in progress, more features to come when I have time to study pipewire and time to implement things. 5 | 6 | ## The packaged version of the program is available at 7 | - [OBS - openSUSE](https://build.opensuse.org/package/show/multimedia:proaudio/pipecontrol) 8 | - [AUR - Archlinux](https://aur.archlinux.org/packages/pipecontrol) 9 | 10 | ## GeekosDAW 11 | The project is developed in collaboration with the **Geekos DAW** community 12 | 13 | https://geekosdaw.tuxfamily.org/en/ 14 | -------------------------------------------------------------------------------- /qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | resources/main.qml 4 | qtquickcontrols2.conf 5 | resources/Top.qml 6 | resources/Settings.qml 7 | 8 | 9 | -------------------------------------------------------------------------------- /qtquickcontrols2.conf: -------------------------------------------------------------------------------- 1 | [Controls] 2 | Style=org.kde.desktop 3 | -------------------------------------------------------------------------------- /resources/PipeControl.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Categories=AudioVideo;Music; 3 | Name=PipeControl 4 | Exec=${CMAKE_INSTALL_PREFIX}/bin/pipecontrol %u 5 | Type=Application 6 | Terminal=false 7 | Icon=pipecontrol 8 | -------------------------------------------------------------------------------- /resources/Settings.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Layouts 1.11 3 | import QtQuick.Controls 2.15 4 | import org.kde.kirigami 2.15 as Kirigami 5 | import Pipewire 1.0 6 | 7 | Kirigami.ScrollablePage { 8 | id: settingsPage 9 | title: i18nc("@title","Settings") 10 | property var root 11 | property bool show_advanced: false 12 | 13 | 14 | actions: [ 15 | Kirigami.Action { 16 | id: advanced_settings 17 | text: i18nc("@settings","advanced") 18 | icon.name: "adjustrow" 19 | checkable: true 20 | checked: settingsPage.show_advanced 21 | onCheckedChanged: settingsPage.show_advanced = advanced_settings.checked 22 | }, 23 | Kirigami.Action { 24 | text: i18n("Top") 25 | icon.name: "source-playlist" 26 | onTriggered: root.pageStack.replace("qrc:/resources/Top.qml", { 27 | root: root 28 | }) 29 | } 30 | ] 31 | 32 | Item { 33 | width: layout.implicitWidth 34 | height: layout.implicitHeight 35 | implicitWidth: layout.implicitWidth 36 | implicitHeight: layout.implicitHeight 37 | Kirigami.FormLayout { 38 | anchors.fill: parent 39 | id: layout 40 | 41 | ComboBox { 42 | Kirigami.FormData.label: i18n("Force sample rate") 43 | 44 | id: forceRateCombo 45 | textRole: "text" 46 | valueRole: "value" 47 | onActivated: Pipewire.settings.force_sampleRate = currentValue 48 | Component.onCompleted: { 49 | update(); 50 | Pipewire.settings.force_sampleRateChanged.connect(update); 51 | } 52 | 53 | function update() { 54 | currentIndex = indexOfValue(Pipewire.settings.force_sampleRate) 55 | } 56 | 57 | model: [ 58 | { value: 0, text: qsTr("Do not force") }, 59 | { value: 44100, text: "44100" }, 60 | { value: 48000, text: "48000" }, 61 | { value: 88200, text: "88200" }, 62 | { value: 96000, text: "96000" }, 63 | { value: 176400, text: "176400" }, 64 | { value: 192000, text: "192000" } 65 | ] 66 | } 67 | 68 | ComboBox { 69 | Kirigami.FormData.label: i18n("Minimum buffer") 70 | id: minRateCombo 71 | textRole: "text" 72 | valueRole: "value" 73 | onActivated: Pipewire.settings.minBuffer = currentValue 74 | Component.onCompleted: { 75 | update(); 76 | Pipewire.settings.minBufferChanged.connect(update); 77 | } 78 | 79 | function update() { 80 | currentIndex = indexOfValue(Pipewire.settings.minBuffer) 81 | } 82 | 83 | model: [ 84 | { value: 8, text: "8" }, 85 | { value: 16, text: "16" }, 86 | { value: 32, text: "32" }, 87 | { value: 64, text: "64" }, 88 | { value: 128, text: "128" }, 89 | { value: 256, text: "256" }, 90 | { value: 512, text: "512" }, 91 | { value: 1024, text: "1024" }, 92 | { value: 2048, text: "2048" }, 93 | { value: 4096, text: "4096" }, 94 | { value: 8192, text: "8192" }, 95 | ] 96 | } 97 | 98 | ComboBox { 99 | Kirigami.FormData.label: i18n("Max buffer") 100 | id: maxBufferCombo 101 | textRole: "text" 102 | valueRole: "value" 103 | onActivated: Pipewire.settings.maxBuffer = currentValue 104 | Component.onCompleted: { 105 | update(); 106 | Pipewire.settings.maxBufferChanged.connect(update); 107 | } 108 | 109 | function update() { 110 | currentIndex = indexOfValue(Pipewire.settings.maxBuffer) 111 | } 112 | 113 | model: [ 114 | { value: 8, text: "8" }, 115 | { value: 16, text: "16" }, 116 | { value: 32, text: "32" }, 117 | { value: 64, text: "64" }, 118 | { value: 128, text: "128" }, 119 | { value: 256, text: "256" }, 120 | { value: 512, text: "512" }, 121 | { value: 1024, text: "1024" }, 122 | { value: 2048, text: "2048" }, 123 | { value: 4096, text: "4096" }, 124 | { value: 8192, text: "8192" }, 125 | ] 126 | } 127 | 128 | ComboBox { 129 | Kirigami.FormData.label: i18n("Force buffer size") 130 | id: forceBufferCombo 131 | textRole: "text" 132 | valueRole: "value" 133 | onActivated: Pipewire.settings.force_buffer = currentValue 134 | Component.onCompleted: { 135 | update(); 136 | Pipewire.settings.force_bufferChanged.connect(update); 137 | } 138 | 139 | function update() { 140 | currentIndex = indexOfValue(Pipewire.settings.force_buffer) 141 | } 142 | 143 | model: [ 144 | { value: 0, text: qsTr("Do not force") }, 145 | { value: 8, text: "8" }, 146 | { value: 16, text: "16" }, 147 | { value: 32, text: "32" }, 148 | { value: 64, text: "64" }, 149 | { value: 128, text: "128" }, 150 | { value: 256, text: "256" }, 151 | { value: 512, text: "512" }, 152 | { value: 1024, text: "1024" }, 153 | { value: 2048, text: "2048" }, 154 | { value: 4096, text: "4096" }, 155 | { value: 8192, text: "8192" }, 156 | ] 157 | } 158 | 159 | ComboBox { 160 | visible: settingsPage.show_advanced 161 | Kirigami.FormData.label: i18n("Log Level") 162 | id: logLevelCombo 163 | textRole: "text" 164 | valueRole: "value" 165 | onActivated: Pipewire.settings.logLevel = currentValue 166 | Component.onCompleted: { 167 | update(); 168 | Pipewire.settings.logLevelChanged.connect(update); 169 | } 170 | 171 | function update() { 172 | currentIndex = indexOfValue(Pipewire.settings.logLevel) 173 | } 174 | 175 | model: [ 176 | { value: 0, text: qsTr("No log") }, 177 | { value: 1, text: qsTr("Error") }, 178 | { value: 2, text: qsTr("Warning")}, 179 | { value: 3, text: qsTr("Info") }, 180 | { value: 4, text: qsTr("Debug") }, 181 | { value: 5, text: qsTr("Trace") }, 182 | ] 183 | } 184 | 185 | // Pipewire Media Session SECTION --------------------------------- 186 | Kirigami.Separator { 187 | visible: Pipewire.isPipewireMediaSession() && settingsPage.show_advanced 188 | Kirigami.FormData.isSection: true 189 | Kirigami.FormData.label: i18n("Pipewire media session") 190 | } 191 | 192 | Switch { 193 | visible: Pipewire.isPipewireMediaSession() && settingsPage.show_advanced 194 | Kirigami.FormData.label: i18n("Alsa Batching (USB Device) extra buffer") 195 | text: checked ? "enabled" : "disabled" 196 | checked: !Pipewire.alsaProperties.batchDisabled 197 | onCheckedChanged: Pipewire.alsaProperties.batchDisabled = !checked 198 | } 199 | 200 | TextField { 201 | visible: Pipewire.isPipewireMediaSession() && settingsPage.show_advanced 202 | Kirigami.FormData.label: i18n("\"Alsa Batch\" extra buffer") 203 | text: Pipewire.alsaProperties.periodSize 204 | onTextChanged: Pipewire.alsaProperties.periodSize = text 205 | } 206 | 207 | Button { 208 | visible: Pipewire.isPipewireMediaSession() && settingsPage.show_advanced 209 | Kirigami.FormData.label: i18n("Session service") 210 | text: "Restart" 211 | onClicked: Pipewire.pipewireMediaSession.restart() 212 | } 213 | 214 | // WirePlumber SECTION -------------------------------------------- 215 | Kirigami.Separator { 216 | visible: Pipewire.isWireplumber() && settingsPage.show_advanced 217 | Kirigami.FormData.isSection: true 218 | Kirigami.FormData.label: i18n("Wireplumber") 219 | } 220 | 221 | Button { 222 | visible: Pipewire.isWireplumber() && settingsPage.show_advanced 223 | Kirigami.FormData.label: i18n("Session service") 224 | text: "Restart" 225 | onClicked: Pipewire.wirePlumberService.restart() 226 | } 227 | 228 | //Kirigami.Separator { 229 | // visible: settingsPage.show_advanced 230 | // Kirigami.FormData.isSection: true 231 | // Kirigami.FormData.label: i18n("Pipewire client properties") 232 | //} 233 | 234 | //ComboBox { 235 | // visible: settingsPage.show_advanced 236 | // Kirigami.FormData.label: i18n("Client Properties") 237 | // id: clientBox 238 | // textRole: "name" 239 | // valueRole: "value" 240 | // Component.onCompleted: { 241 | // currentIndex = 0 242 | // } 243 | 244 | // model: Pipewire.client.propertiesList 245 | //} 246 | 247 | //Label { 248 | // visible: settingsPage.show_advanced 249 | // text: clientBox.currentValue 250 | //} 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /resources/Top.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Layouts 1.11 3 | import Pipewire 1.0 4 | import Pipewire.Node 1.0 5 | import QtQuick.Controls 2.15 as Controls 6 | import org.kde.kirigami 2.15 as Kirigami 7 | 8 | Kirigami.ScrollablePage { 9 | title: i18nc("@title","Top") 10 | property var root 11 | Layout.fillWidth: true 12 | 13 | actions: [ 14 | Kirigami.Action { 15 | id: addAction 16 | icon.name: "settings-configure" 17 | text: i18n("Settings") 18 | onTriggered: root.pageStack.replace("qrc:/resources/Settings.qml", { 19 | root: root 20 | }) 21 | } 22 | ] 23 | 24 | ListView { 25 | id: mainList 26 | Layout.fillWidth: trueq 27 | Layout.fillHeight: true 28 | currentIndex: -1 29 | 30 | header: RowLayout { 31 | id: header 32 | height: 30 33 | Layout.fillWidth: true 34 | 35 | Kirigami.Icon {} 36 | 37 | Controls.Label { 38 | Layout.leftMargin: 20 39 | Layout.preferredWidth: 40 40 | Layout.alignment: Qt.AlignRight 41 | text: "ID" 42 | } 43 | Rectangle { 44 | Layout.preferredWidth: 50 45 | Controls.Label { 46 | anchors.verticalCenter: parent.verticalCenter 47 | anchors.right: parent.right 48 | text: "RATE" 49 | } 50 | } 51 | Rectangle { 52 | Layout.preferredWidth: 70 53 | Controls.Label { 54 | anchors.verticalCenter: parent.verticalCenter 55 | anchors.right: parent.right 56 | text: "QUANT" 57 | } 58 | } 59 | Rectangle { 60 | Layout.preferredWidth: 70 61 | Controls.Label { 62 | anchors.verticalCenter: parent.verticalCenter 63 | anchors.right: parent.right 64 | text: "WAIT" 65 | } 66 | } 67 | Rectangle { 68 | Layout.preferredWidth: 70 69 | Controls.Label { 70 | anchors.verticalCenter: parent.verticalCenter 71 | anchors.right: parent.right 72 | text: "BUSY" 73 | } 74 | } 75 | 76 | Controls.Label { 77 | Layout.leftMargin: 15 78 | Layout.fillWidth: true 79 | text: "NAME" 80 | } 81 | } 82 | 83 | model: Pipewire.nodes 84 | Component.onCompleted: Pipewire.nodes.sortList() 85 | property var old_model: ListModel { 86 | id: listModel 87 | 88 | Component.onCompleted: { 89 | for (var i = 0; i < 200; ++i) { 90 | listModel.append({"title": "Item " + i, 91 | "actions": [{text: "Action 1", icon: "document-decrypt"}, 92 | {text: "Action 2", icon: "mail-reply-sender"}], 93 | //"sec": Math.floor(i/10) 94 | }) 95 | } 96 | } 97 | } 98 | moveDisplaced: Transition { 99 | YAnimator { 100 | duration: Kirigami.Units.longDuration 101 | easing.type: Easing.InOutQuad 102 | } 103 | } 104 | reuseItems: true 105 | delegate: Kirigami.SwipeListItem { 106 | id: listItem 107 | contentItem: RowLayout { 108 | id: noderowcomponent 109 | opacity: model.node.state === "RUNNING" ? 1.0 : 0.5 110 | 111 | Kirigami.ListItemDragHandle { 112 | visible: false //model.driverID > 0 113 | listItem: listItem 114 | listView: mainList 115 | property int oldIndex: -1; 116 | property int newIndex: -1; 117 | onMoveRequested: function(p_oldIndex, p_newIndex) { 118 | oldIndex = p_oldIndex 119 | newIndex = p_newIndex 120 | } 121 | onDropped: { 122 | console.log("element moved " + oldIndex + " -> " + newIndex + " dropped"); 123 | Pipewire.nodes.move(oldIndex, newIndex); 124 | oldIndex = -1 125 | newIndex = -1 126 | } 127 | } 128 | 129 | Kirigami.Icon { 130 | id: nodeIcon 131 | Controls.ToolTip.visible: hovered 132 | Controls.ToolTip.text: textSource(model.node) 133 | 134 | 135 | source: mediaIconSource(model.node) 136 | fallback: "script-error" 137 | 138 | 139 | function mediaIconSource(node) { 140 | switch(node.mediaType) { 141 | case Node.MediaTypeAudio: 142 | switch(node.nodeType) { 143 | case Node.NodeTypeInput: 144 | case Node.NodeTypeSource: 145 | return "audio-input-microphone-symbolic"; 146 | case Node.NodeTypeOutput: 147 | case Node.NodeTypeSink: 148 | return "audio-speakers-symbolic"; 149 | default: 150 | return "error"; 151 | } 152 | case Node.MediaTypeVideo: 153 | return "camera-video-symbolic"; 154 | case Node.MediaTypeMidi: 155 | return "music-note-16th"; 156 | default: 157 | case Node.MediaTypeNone: 158 | return "error"; 159 | } 160 | } 161 | 162 | function textSource(node) { 163 | switch(node.mediaType) { 164 | case Node.MediaTypeAudio: 165 | switch(node.nodeType) { 166 | case Node.NodeTypeInput: 167 | return i18nc("node_type", "Audio Input"); 168 | case Node.NodeTypeSource: 169 | return i18nc("node_type", "Audio Source"); 170 | case Node.NodeTypeOutput: 171 | return i18nc("node_type", "Audio Output"); 172 | case Node.NodeTypeSink: 173 | return i18nc("node_type", "Audio Sink"); 174 | default: 175 | return i18nc("node_type", "Error"); 176 | } 177 | case Node.MediaTypeVideo: 178 | switch(node.nodeType) { 179 | case Node.NodeTypeInput: 180 | return i18nc("node_type", "Video Input"); 181 | case Node.NodeTypeSource: 182 | return i18nc("node_type", "Video Source"); 183 | case Node.NodeTypeOutput: 184 | return i18nc("node_type", "Video Output"); 185 | case Node.NodeTypeSink: 186 | return i18nc("node_type", "Video Sink"); 187 | default: 188 | return i18nc("node_type", "Error"); 189 | } 190 | case Node.MediaTypeMidi: 191 | return i18nc("node_type", "Midi"); 192 | case Node.MediaTypeNone: 193 | return i18nc("node_type", "None"); 194 | default: 195 | return i18nc("node_type", "Error"); 196 | } 197 | } 198 | } 199 | 200 | Controls.Label { 201 | Layout.leftMargin: 10 202 | Layout.preferredWidth: 40 203 | Layout.alignment: Qt.AlignRight 204 | text: model.id 205 | } 206 | 207 | Rectangle { 208 | Layout.preferredWidth: 50 209 | Layout.alignment: Qt.AlignRight; 210 | Controls.Label { 211 | anchors.verticalCenter: parent.verticalCenter 212 | anchors.right: parent.right 213 | text: rate 214 | } 215 | } 216 | Rectangle { 217 | Layout.preferredWidth: 70 218 | Layout.alignment: Qt.AlignRight; 219 | Controls.Label { 220 | anchors.verticalCenter: parent.verticalCenter 221 | anchors.right: parent.right 222 | text: quantum 223 | } 224 | } 225 | Rectangle { 226 | Layout.preferredWidth: 70 227 | Controls.Label { 228 | anchors.verticalCenter: parent.verticalCenter 229 | anchors.right: parent.right 230 | // text: node.volume.toFixed(4) 231 | text: Pipewire.formatTime(wait) 232 | } 233 | } 234 | Rectangle { 235 | Layout.preferredWidth: 70 236 | Controls.Label { 237 | anchors.verticalCenter: parent.verticalCenter 238 | anchors.right: parent.right 239 | text: Pipewire.formatTime(busy) 240 | } 241 | } 242 | 243 | RowLayout { 244 | Layout.leftMargin: 15 245 | Layout.fillWidth: true 246 | Kirigami.Icon { 247 | visible: model.driverID > 0 248 | source: "aggregation" 249 | } 250 | 251 | ColumnLayout { 252 | Layout.fillWidth: true 253 | Layout.fillHeight: true 254 | Controls.Label { 255 | //Controls.ToolTip.visible: model.node.isAlsa() && hovered 256 | //Controls.ToolTip.text: model.node.nodeDescription 257 | Layout.fillWidth: true 258 | height: Math.max(implicitHeight, Kirigami.Units.iconSizes.smallMedium) 259 | text: model.name //+ "|" + model.display.category + "|" + model.display.mediaClass 260 | //color: listItem.checked || (listItem.pressed && !listItem.checked && !listItem.sectionDelegate) ? listItem.activeTextColor : listItem.textColor 261 | } 262 | Controls.Label { 263 | visible: model.node.nodeDescription && model.name != model.node.nodeDescription 264 | Layout.fillWidth: true 265 | height: Math.max(implicitHeight, Kirigami.Units.iconSizes.smallMedium) 266 | text: model.node.nodeDescription 267 | color: Kirigami.Theme.disabledTextColor 268 | } 269 | } 270 | } 271 | 272 | Kirigami.OverlaySheet { 273 | id: infoAlsaNodeSheet 274 | anchors.centerIn: Controls.Overlay.overlay 275 | width: infoAlsaNodeSheet_form.implicitWidth + 40 276 | height: infoAlsaNodeSheet_form.implicitHeight + 40 277 | header: Kirigami.Heading { 278 | text: model.name 279 | } 280 | Kirigami.FormLayout { 281 | id: infoAlsaNodeSheet_form 282 | implicitWidth: Math.max(nameField.implicitWidth, descriptionField.implicitWidth, classField.implicitWidth, runningField.implicitWidth) 283 | implicitHeight: Math.max(200, nameField.implicitHeight + descriptionField.implicitHeight + classField.implicitHeight + runningField.implicitHeight + (40*5)) 284 | Controls.Label { 285 | id: nameField 286 | Kirigami.FormData.label: i18nc("@label:textbox", "Node Description:") 287 | text: model.node.nodeDescription 288 | enabled: false 289 | } 290 | Controls.Label { 291 | id: descriptionField 292 | Kirigami.FormData.label: i18nc("@label:textbox", "Node name:") 293 | text: model.node.nodeName 294 | enabled: false 295 | } 296 | Controls.Label { 297 | id: classField 298 | Kirigami.FormData.label: i18nc("@label:textbox", "Media Class:") 299 | text: model.node.mediaClass 300 | enabled: false 301 | } 302 | Controls.Label { 303 | id: runningField 304 | Kirigami.FormData.label: i18nc("@label:stateLabel", "State:") 305 | text: i18nc("@label:state", model.node.state) 306 | enabled: false 307 | } 308 | } 309 | } 310 | } 311 | actions: [ 312 | Kirigami.Action { 313 | visible: model.node.isAlsa() 314 | icon.name: "documentinfo" 315 | text: i18nc("@audiostream","info") 316 | onTriggered: { 317 | infoAlsaNodeSheet.open() 318 | } 319 | } 320 | ] 321 | } 322 | //section { 323 | //property: "sec" 324 | //delegate: Kirigami.ListSectionHeader { 325 | //text: "Section " + (parseInt(section) + 1) 326 | //} 327 | //} 328 | } 329 | 330 | 331 | } 332 | -------------------------------------------------------------------------------- /resources/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.15 2 | import QtQuick.Controls 2.15 as Controls 3 | import QtQuick.Layouts 1.15 4 | import org.kde.kirigami 2.15 as Kirigami 5 | import Pipewire 1.0 6 | 7 | 8 | Kirigami.ApplicationWindow { 9 | id: root 10 | visible: true 11 | title: "PipeControl" 12 | 13 | globalDrawer: Kirigami.GlobalDrawer { 14 | isMenu: true 15 | actions: [ 16 | Kirigami.Action { 17 | text: i18n("Top") 18 | icon.name: "source-playlist" 19 | onTriggered: root.pageStack.replace("qrc:/resources/Top.qml", { 20 | root: root 21 | }) 22 | }, 23 | Kirigami.Action { 24 | text: i18n("Settings") 25 | icon.name: "settings-configure" 26 | onTriggered: root.pageStack.replace("qrc:/resources/Settings.qml", { 27 | root: root 28 | }) 29 | }, 30 | Kirigami.Action { 31 | text: i18n("About") 32 | icon.name: "help-about-symbolic" 33 | onTriggered: aboutSheet.open() 34 | }, 35 | Kirigami.Action { 36 | text: i18n("Quit") 37 | icon.name: "window-close" 38 | shortcut: StandardKey.Quit 39 | onTriggered: Qt.quit() 40 | } 41 | ] 42 | } 43 | 44 | Kirigami.OverlaySheet { 45 | id: aboutSheet 46 | width: aboutlabel.leftPadding + aboutlabel.contentWidth + aboutlabel.rightPadding + 40 // 40 extra necessary, unknown reason 47 | parent: applicationWindow().overlay 48 | header: Kirigami.Heading { 49 | text: "About PipeControl" 50 | } 51 | RowLayout { 52 | Controls.Label { 53 | id: aboutlabel 54 | text: "Pipecontrol v" + Pipewire.appVersion + 55 | "\nWork in progress by @portaloffreedom" + 56 | "\nLicense: GPLv3" + 57 | "\n" + 58 | "\n Pipewire compiled version: " + Pipewire.pipewireCompiledVersion() + 59 | "\n Pipewire linked version: " + Pipewire.pipewireLinkedVersion() + 60 | "\n Qt compiled version: " + Pipewire.qtCompiledVersion() + 61 | "\n Qt linked version: " + Pipewire.qtLinkedVersion() + 62 | "\n KF5 compiled version: " + Pipewire.kframeworksCompiledVersion() + 63 | "\n Platform: " + Pipewire.platformName() 64 | } 65 | } 66 | } 67 | 68 | 69 | // Initial page to be loaded on app load 70 | pageStack.initialPage: Top { 71 | root: root 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /resources/pipecontrol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /resources/translations/pipecontrol_it_IT.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Settings 6 | 7 | 8 | 9 | Do not force 10 | 11 | 12 | 13 | 14 | No log 15 | 16 | 17 | 18 | 19 | Error 20 | 21 | 22 | 23 | 24 | Warning 25 | 26 | 27 | 28 | 29 | Info 30 | 31 | 32 | 33 | 34 | Debug 35 | 36 | 37 | 38 | 39 | Trace 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #include "src/pw/qpipewire.h" 29 | #include "src/pw/qpipewireclient.h" 30 | 31 | int main(int argc, char *argv[]) 32 | { 33 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 34 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 35 | QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 36 | #endif 37 | 38 | QGuiApplication app(argc, argv); 39 | KLocalizedString::setApplicationDomain("pipecontrol"); 40 | QCoreApplication::setOrganizationName(QStringLiteral("Dek")); 41 | QCoreApplication::setOrganizationDomain(QStringLiteral("matteodroids.science")); 42 | QCoreApplication::setApplicationName(QStringLiteral("PipeControl")); 43 | 44 | QGuiApplication::setApplicationVersion(QStringLiteral(PIPECONTROL_VERSION)); 45 | QGuiApplication::setQuitOnLastWindowClosed(true); 46 | QGuiApplication::setWindowIcon(QIcon(QStringLiteral(INSTALL_PREFIX"/share/icons/hicolor/scalable/apps/pipecontrol.svg"))); 47 | // This is necessary for the Application Icon on wayland (xdg-shell standard) 48 | QGuiApplication::setDesktopFileName(QStringLiteral("PipeControl")); 49 | 50 | QTranslator translator; 51 | const QStringList uiLanguages = QLocale::system().uiLanguages(); 52 | for (const QString &locale : uiLanguages) { 53 | const QString baseName = QStringLiteral("pipecontrol_") + QLocale(locale).name(); 54 | if (translator.load(QStringLiteral(":/i18n/") + baseName)) { 55 | QGuiApplication::installTranslator(&translator); 56 | break; 57 | } 58 | } 59 | 60 | // Allocate before the engine to ensure that it outlives it 61 | QPipewire *qpipewire = new QPipewire(&argc, &argv); 62 | qpipewire->round_trip(); 63 | 64 | QTimer timer; 65 | QObject::connect(&timer, &QTimer::timeout, qpipewire, &QPipewire::round_trip); 66 | timer.start(100); 67 | 68 | QObject::connect(qpipewire, &QPipewire::quit, &app, &QGuiApplication::quit); 69 | 70 | #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) 71 | qmlRegisterSingletonInstance("Pipewire", 1, 0, "Pipewire", qpipewire); 72 | qmlRegisterAnonymousType("Pipewire.Client", 1); 73 | #else 74 | static QPipewire *s_qpipewire = qpipewire; 75 | qmlRegisterSingletonType("Pipewire", 1, 0, "Pipewire", [](QQmlEngine*, QJSEngine*) {return static_cast(s_qpipewire);}); 76 | #endif 77 | 78 | qmlRegisterUncreatableType("Pipewire.Node", 1, 0, "Node", QStringLiteral("Not creatable from QML")); 79 | 80 | 81 | QQmlApplicationEngine engine; 82 | engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); 83 | const QUrl urlMain(QStringLiteral("qrc:/resources/main.qml")); 84 | QObject::connect(&engine, 85 | &QQmlApplicationEngine::objectCreated, 86 | &app, 87 | [urlMain](const QObject *obj, const QUrl &objUrl) { 88 | if (!obj && urlMain == objUrl) 89 | QCoreApplication::exit(-1); 90 | }, 91 | Qt::QueuedConnection); 92 | engine.load(urlMain); 93 | 94 | if (engine.rootObjects().isEmpty()) { 95 | qCritical("Engine did not load any root object"); 96 | return -1; 97 | } 98 | 99 | int ret = QGuiApplication::exec(); 100 | 101 | #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) 102 | // when registering a type in `qmlRegisterSingletonType`, the object is deleted automatically by the qt engine 103 | // otherwise we delete it ourselves 104 | delete qpipewire; 105 | #endif 106 | return ret; 107 | } 108 | -------------------------------------------------------------------------------- /src/pw/media-session/alsaproperties.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "alsaproperties.h" 18 | #include "src/pw/qpipewireclient.h" 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | bool is_valid_file(const QString &path) 29 | { 30 | QFile file(path); 31 | if (!file.exists()) return false; 32 | file.open(QIODevice::ReadOnly | QIODevice::Text); 33 | return file.isReadable(); 34 | } 35 | 36 | AlsaProperties::AlsaProperties(QPipewireClient *client, QObject *parent) 37 | : QObject(parent) 38 | { 39 | std::string config_prefix = client->has_property(QStringLiteral("config.prefix")) ? 40 | client->property(QStringLiteral("config.prefix")).toStdString() : "media-session.d"; 41 | const char *home_folder = std::getenv("HOME"); 42 | if (home_folder == nullptr) { 43 | home_folder = ""; 44 | } 45 | 46 | std::ostringstream global_conf_filename; 47 | global_conf_filename << "/usr/share/pipewire/" 48 | << config_prefix 49 | << "/alsa-monitor.conf"; 50 | std::ostringstream user_conf_filename; 51 | user_conf_filename << home_folder 52 | << "/.config/pipewire/" 53 | << config_prefix 54 | << "/alsa-monitor.conf"; 55 | 56 | this->globalConf = QString::fromUtf8(global_conf_filename.str().c_str()); 57 | this->userConf = QString::fromUtf8(user_conf_filename.str().c_str()); 58 | 59 | bool has_global = is_valid_file(globalConf); 60 | bool has_user = is_valid_file(userConf); 61 | 62 | if (has_user) { 63 | this->readUserConf(); 64 | } else if (has_global) { 65 | this->readGlobalConf(); 66 | } else { 67 | throw std::runtime_error("Could not find pipewire alsa monitor config file!"); 68 | } 69 | } 70 | 71 | void AlsaProperties::setBatchDisabled(bool disabled) 72 | { 73 | if (_batchDisabled == disabled) return; 74 | _batchDisabled = disabled; 75 | writeUserConf(); 76 | Q_EMIT batchDisabledChanged(_batchDisabled); 77 | } 78 | 79 | void AlsaProperties::setPeriodSize(int newPeriod) 80 | { 81 | if (_periodSize == newPeriod) return; 82 | _periodSize = newPeriod; 83 | writeUserConf(); 84 | Q_EMIT periodSizeChanged(_periodSize); 85 | } 86 | 87 | void AlsaProperties::readGlobalConf() 88 | { 89 | readConf(globalConf); 90 | } 91 | 92 | void AlsaProperties::readUserConf() 93 | { 94 | readConf(userConf); 95 | } 96 | 97 | template 98 | T _parse(const QString &/*value*/) { 99 | throw std::runtime_error("Unsupported conversion"); 100 | } 101 | template<> 102 | int _parse(const QString &value) { 103 | return value.toInt(); 104 | } 105 | template<> 106 | bool _parse(const QString &value) { 107 | const QString lower_case_value = value.toLower(); 108 | if (lower_case_value == QStringLiteral("true")) { 109 | return true; 110 | } else if (lower_case_value == QStringLiteral("false")) { 111 | return false; 112 | } else { 113 | throw std::runtime_error(QString((QStringLiteral("Could not parse bool type from: ") + value)).toStdString()); 114 | } 115 | } 116 | 117 | template 118 | QString _tostring(const T /*value*/) { 119 | throw std::runtime_error("diocane"); 120 | } 121 | template<> 122 | QString _tostring(const int value) { 123 | return QString::number(value); 124 | } 125 | template<> 126 | QString _tostring(const bool value) { 127 | if (value) { 128 | return QStringLiteral("true"); 129 | } else { 130 | return QStringLiteral("false"); 131 | } 132 | } 133 | 134 | template 135 | bool parse_line(const QString &line, const QString &target, T &value) 136 | { 137 | QRegularExpression re(QStringLiteral("(#*)\\s*") + target + QStringLiteral("\\s*=\\s*([\\d|\\w]+)")); 138 | QRegularExpressionMatch match = re.match(line); 139 | if(!match.hasMatch()) { 140 | throw std::runtime_error(QString((QStringLiteral("Could not find ") + target)).toStdString()); 141 | } 142 | 143 | if (!match.captured(1).isEmpty()) { 144 | return false; 145 | } 146 | 147 | value = _parse(match.captured(2)); 148 | return true; 149 | } 150 | 151 | template 152 | void parse_and_change_line(QString &line, const QString &target, T value) 153 | { 154 | QRegularExpression re(QStringLiteral("(#*)\\s*") + target + QStringLiteral("\\s*=\\s*([\\d|\\w]+)")); 155 | QRegularExpressionMatch match = re.match(line); 156 | if(!match.hasMatch()) { 157 | throw std::runtime_error(QString((QStringLiteral("Could not find ") + target)).toStdString()); 158 | } 159 | 160 | if (!match.captured(1).isEmpty()) { 161 | line.remove(match.capturedStart(1), match.capturedLength(1)); 162 | match = re.match(line); 163 | } 164 | 165 | line.remove(match.capturedStart(2), match.capturedLength(2)); 166 | line.insert(match.capturedStart(2), _tostring(value)); 167 | return; 168 | } 169 | 170 | void AlsaProperties::readConf(const QString& filename) 171 | { 172 | QFile conf(filename); 173 | if (!conf.open(QIODevice::ReadOnly | QIODevice::Text)) { 174 | std::ostringstream error_message; 175 | error_message << "Could not open configuration file " << filename.toStdString(); 176 | std::cerr << error_message.str() << std::endl; 177 | throw std::runtime_error(error_message.str()); 178 | } 179 | 180 | while (!conf.atEnd()) { 181 | QString line = QString::fromUtf8(conf.readLine().trimmed()); 182 | 183 | // Skip comments 184 | if (line.startsWith(QStringLiteral("#"))) continue; 185 | 186 | if (line.contains(QStringLiteral("api.alsa.disable-batch"))) { 187 | bool found = parse_line(line, QStringLiteral("api.alsa.disable-batch"), _batchDisabled); 188 | if (found) Q_EMIT batchDisabledChanged(_batchDisabled); 189 | } else if (line.contains(QStringLiteral("api.alsa.period-size"))) { 190 | bool found = parse_line(line, QStringLiteral("api.alsa.period-size"), _periodSize); 191 | if (found) Q_EMIT periodSizeChanged(_periodSize); 192 | } 193 | } 194 | } 195 | 196 | void AlsaProperties::writeUserConf() const 197 | { 198 | QFileInfo confInfo(userConf); 199 | QDir folder = QDir(confInfo.absolutePath()); 200 | std::cout << "Creating user conf folder: " << folder.absolutePath().toStdString() << std::endl; 201 | // This will return true even if the folder already exists. 202 | if (!folder.mkpath(QStringLiteral("."))) { 203 | throw std::runtime_error("Could not create user conf folder"); 204 | } 205 | 206 | // Read file 207 | QList lines; 208 | { 209 | QFile userConfF(userConf); 210 | QFile globalConfF(globalConf); 211 | QFile *inputFile = &userConfF; 212 | if (!userConfF.exists()) { 213 | inputFile = &globalConfF; 214 | } 215 | 216 | if (!inputFile->open(QIODevice::ReadOnly | QIODevice::Text)) { 217 | throw std::runtime_error("Could not write user conf file"); 218 | } 219 | 220 | while (!inputFile->atEnd()) { 221 | QString line = QString::fromUtf8(inputFile->readLine()); 222 | QString trimmed = line.trimmed(); 223 | if (trimmed.contains(QStringLiteral("api.alsa.disable-batch"))) { 224 | parse_and_change_line(line, QStringLiteral("api.alsa.disable-batch"), this->_batchDisabled); 225 | } else if (trimmed.contains(QStringLiteral("api.alsa.period-size"))) { 226 | parse_and_change_line(line, QStringLiteral("api.alsa.period-size"), this->_periodSize); 227 | } 228 | 229 | lines.append(line); 230 | } 231 | 232 | inputFile->close(); 233 | } 234 | 235 | std::cout << "Writing user conf file: " << folder.absolutePath().toStdString() << std::endl; 236 | 237 | { 238 | // Write with lines changed 239 | QFile outFile(userConf); 240 | if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) { 241 | throw std::runtime_error("Could not write user conf file"); 242 | } 243 | 244 | QTextStream out(&outFile); 245 | for(QString &line : lines) { 246 | out << line; 247 | } 248 | 249 | outFile.close(); 250 | } 251 | 252 | } 253 | -------------------------------------------------------------------------------- /src/pw/media-session/alsaproperties.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #pragma once 18 | #include 19 | #include 20 | 21 | class QPipewireClient; 22 | 23 | class AlsaProperties : public QObject 24 | { 25 | Q_OBJECT 26 | Q_PROPERTY(bool batchDisabled READ batchDisabled WRITE setBatchDisabled NOTIFY batchDisabledChanged) 27 | Q_PROPERTY(int periodSize READ periodSize WRITE setPeriodSize NOTIFY periodSizeChanged) 28 | Q_SIGNALS: 29 | void batchDisabledChanged(bool); 30 | void periodSizeChanged(int); 31 | 32 | private: 33 | QString globalConf; 34 | QString userConf; 35 | 36 | bool _batchDisabled = false; 37 | int _periodSize = 1024; 38 | 39 | public: 40 | explicit AlsaProperties(QPipewireClient *client, QObject *parent = nullptr); 41 | virtual ~AlsaProperties() = default; 42 | 43 | [[nodiscard]] bool batchDisabled() const { return _batchDisabled; } 44 | [[nodiscard]] int periodSize() const { return _periodSize; } 45 | void setBatchDisabled(bool disabled); 46 | void setPeriodSize(int newPeriod); 47 | 48 | private: 49 | void readGlobalConf(); 50 | void readUserConf(); 51 | void readConf(const QString &filename); 52 | void writeUserConf() const; 53 | }; 54 | -------------------------------------------------------------------------------- /src/pw/qpipewire.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "qpipewire.h" 18 | #include "src/pw/qpipewiresettings.h" 19 | #include "src/pw/qpipewireclient.h" 20 | #include "src/pw/qpipewirenode.h" 21 | #include "src/pw/qpipewirelink.h" 22 | #include "src/pw/qpipewirealsanode.h" 23 | #include "src/pw/qpipewireport.h" 24 | #include "src/pw/qpipewiredevice.h" 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include 32 | #include 33 | #include 34 | 35 | #include 36 | 37 | #include 38 | #include 39 | 40 | #define QPIPEWIRE_CAST(x) QPipewire* _this = static_cast(x); 41 | 42 | static inline bool streq(const char *s1, const char *s2) 43 | { 44 | return SPA_LIKELY(s1 && s2) ? strcmp(s1, s2) == 0 : s1 == s2; 45 | } 46 | 47 | void QPipewire::round_trip() 48 | { 49 | resync(); 50 | 51 | // The event is collected by callback _on_core_done and 52 | // when proper, it sets round_trip_done to true 53 | while(!round_trip_done) { 54 | pw_main_loop_run(loop); 55 | } 56 | } 57 | 58 | QString QPipewire::formatTime(double val) 59 | { 60 | val *= 1000000000.0f; 61 | QString buf; 62 | if (val < 1000000llu) { 63 | val /= 1000.0f; 64 | buf = QString::asprintf("%5.1fµs", val); 65 | } else if (val < 1000000000llu) { 66 | val /= 1000000.0f; 67 | buf = QString::asprintf("%5.1fms", val); 68 | } else { 69 | val /= 1000000000.0f; 70 | buf = QString::asprintf("%5.1fs", val); 71 | } 72 | return buf; 73 | } 74 | 75 | void QPipewire::resync() 76 | { 77 | round_trip_done = false; 78 | 79 | // This is async, will trigger a DONE event 80 | // with `seq` number the number just returned by this function 81 | sync = pw_core_sync(core, PW_ID_CORE, sync); 82 | } 83 | 84 | //----------------------------------------------------------------------------- 85 | static void do_quit(void *userdata, int /*signal_number*/) 86 | { 87 | QPIPEWIRE_CAST(userdata); 88 | _this->_quit(); 89 | } 90 | 91 | void QPipewire::_quit() 92 | { 93 | pw_main_loop_quit(loop); 94 | Q_EMIT quit(); 95 | } 96 | 97 | //----------------------------------------------------------------------------- 98 | static void on_core_done(void *data, uint32_t id, int seq) 99 | { 100 | QPIPEWIRE_CAST(data); 101 | _this->_on_core_done(id, seq); 102 | } 103 | 104 | void QPipewire::_on_core_done(uint32_t id, int seq) 105 | { 106 | //qDebug() << "_on_core_done(" << id << ',' << seq << ')'; 107 | if (id == PW_ID_CORE && sync == seq) { 108 | round_trip_done = true; 109 | pw_main_loop_quit(loop); 110 | } 111 | } 112 | 113 | static void on_core_error(void *data, uint32_t id, int seq, int res, const char *message) 114 | { 115 | QPIPEWIRE_CAST(data); 116 | _this->_on_core_error(id, seq, res, message); 117 | } 118 | 119 | void QPipewire::_on_core_error(u_int32_t id, int seq, int res, const char *message) 120 | { 121 | pw_log_error("error id:%u seq:%d res:%d (%s): %s", 122 | id, seq, res, spa_strerror(res), message); 123 | 124 | if (id == PW_ID_CORE && res == -EPIPE) 125 | pw_main_loop_quit(loop); 126 | } 127 | 128 | static const pw_core_events core_events = { 129 | .version = PW_VERSION_CORE_EVENTS, 130 | .info = nullptr, 131 | .done = on_core_done, 132 | .ping = nullptr, 133 | .error = on_core_error, 134 | .remove_id = nullptr, 135 | .bound_id = nullptr, 136 | .add_mem = nullptr, 137 | .remove_mem = nullptr 138 | }; 139 | 140 | //----------------------------------------------------------------------------- 141 | static void registry_event(void *data, 142 | uint32_t id, 143 | uint32_t permissions, 144 | const char *type, 145 | uint32_t version, 146 | const struct spa_dict *props) 147 | { 148 | QPIPEWIRE_CAST(data); 149 | _this->_registry_event(id, permissions, type, version, props); 150 | } 151 | 152 | static void registry_event_remove(void *data, uint32_t id) 153 | { 154 | QPIPEWIRE_CAST(data); 155 | _this->_registry_event_remove(id); 156 | } 157 | 158 | void QPipewire::_registry_event(uint32_t id, 159 | uint32_t permissions, 160 | const char *type, 161 | uint32_t version, 162 | const struct spa_dict *props) 163 | { 164 | // qDebug() << "object: id(" << id << ") type(" << type << '/' << version << ')'; 165 | 166 | Q_EMIT registryObject(id, permissions, type, version, props); 167 | 168 | if(strcmp(type, PW_TYPE_INTERFACE_Client) == 0) 169 | { 170 | QPipewireClient *pw_client = new QPipewireClient(this, id, props); 171 | // Q_EMIT clientChanged(); 172 | 173 | // Shot only once when data has arrived 174 | QMetaObject::Connection *const connection = new QMetaObject::Connection; 175 | *connection = connect(pw_client, &QPipewireClient::propertiesChanged, [this, pw_client, connection]() { 176 | if (this->isPipewireMediaSession()) { 177 | // creating alsa properties when media-session is not installed could crash the application 178 | alsa_properties = new AlsaProperties(pw_client, this); 179 | Q_EMIT alsaPropertiesChanged(); 180 | } 181 | // delete so the connection is not shot twice 182 | delete connection; 183 | }); 184 | pw_clients.push_back(pw_client); 185 | } 186 | else if (strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) 187 | { 188 | const char *metadata_name; 189 | metadata_name = spa_dict_lookup(props, PW_KEY_METADATA_NAME); 190 | if (metadata_name != nullptr) 191 | { 192 | if (this->pw_settings == nullptr && 193 | streq(metadata_name, "settings")) 194 | { 195 | pw_settings = new QPipewireSettings(this, id, props); 196 | Q_EMIT settingsChanged(); 197 | } else { 198 | // qWarning() << "Ignoring metadata \"" << metadata_name << '"'; 199 | } 200 | } 201 | } 202 | else if (streq(type, PW_TYPE_INTERFACE_Profiler)) 203 | { 204 | if (pw_profiler != nullptr) { 205 | // qWarning() << "Ignoring profiler " << id << ": already attached"; 206 | return; 207 | } 208 | pw_profiler = new QPipewireProfiler(this, id, props); 209 | Q_EMIT profilerChanged(); 210 | } 211 | else if (streq(type, PW_TYPE_INTERFACE_Node)) 212 | { 213 | QPipewireNode *node; 214 | const QString str = QString::fromUtf8(spa_dict_lookup(props, PW_KEY_NODE_NAME)); 215 | if (str.startsWith(QStringLiteral("alsa_input.")) || str.startsWith(QStringLiteral("alsa_output."))) { 216 | node = new QPipewireAlsaNode(this, id, props); 217 | } else { 218 | node = new QPipewireNode(this, id, props); 219 | } 220 | m_nodes->append(node); 221 | // qWarning() << "Adding node id(" << id << "): " << node->name(); 222 | Q_EMIT nodesChanged(); 223 | } 224 | else if (streq(type, PW_TYPE_INTERFACE_Link)) 225 | { 226 | QPipewireLink *link = new QPipewireLink(this, id, props); 227 | m_links.append(link); 228 | // qWarning() << "Adding link id(" << id << "): "; 229 | Q_EMIT linksChanged(); 230 | } 231 | else if (streq(type, PW_TYPE_INTERFACE_Port)) 232 | { 233 | QPipewirePort *port = new QPipewirePort(this, id, props); 234 | m_ports.append(port); 235 | // qWarning() << "Adding port id(" << id << "): "; 236 | Q_EMIT portsChanged(); 237 | } 238 | else if (streq(type, PW_TYPE_INTERFACE_Device)) { 239 | QPipewireDevice *device = new QPipewireDevice(this, id, props); 240 | m_devices.append(device); 241 | } 242 | } 243 | 244 | void QPipewire::_registry_event_remove(uint32_t id) 245 | { 246 | // qWarning() << "Attempting to remove id(" << id << ")"; 247 | for(int i=0; isize(); i++) { 248 | QPipewireNode *candidate = (*m_nodes)[i]; 249 | if (candidate && candidate->id_u32() == id) { 250 | QPipewireNode *node = candidate; 251 | // ALL interfaces that are not NODES are ignored here. 252 | // qWarning() << "Removing id(" << id << ":" << node->id() << "): " << node->name(); 253 | m_nodes->removeAt(i); 254 | Q_EMIT nodesChanged(); 255 | node->deleteLater(); 256 | return; 257 | } 258 | } 259 | 260 | for(int i=0; iid_u32() == id) { 263 | QPipewireLink *link = candidate; 264 | m_links.removeAt(i); 265 | Q_EMIT linksChanged(); 266 | link->deleteLater(); 267 | return; 268 | } 269 | } 270 | 271 | } 272 | 273 | static const pw_registry_events registry_events = { 274 | .version = PW_VERSION_REGISTRY_EVENTS, 275 | .global = registry_event, 276 | .global_remove = registry_event_remove, 277 | }; 278 | 279 | //----------------------------------------------------------------------------- 280 | QPipewire::QPipewire(int *argc, char **argv[], QObject *parent) 281 | : QObject(parent) 282 | , m_nodes(new QPipewireNodeListModel()) 283 | { 284 | spa_zero(core_listener); 285 | spa_zero(registry_listener); 286 | 287 | pw_init(argc, argv); 288 | 289 | qInfo() << "Compiled with libpipewire " << pw_get_headers_version(); 290 | qInfo() << "Linked with libpipewire " << pw_get_library_version(); 291 | 292 | loop = pw_main_loop_new(nullptr); 293 | if (loop == nullptr) { 294 | throw std::runtime_error("Error initializing Pipewire mainloop"); 295 | } 296 | 297 | pw_loop_add_signal(pw_main_loop_get_loop(loop), SIGINT, do_quit, this); 298 | pw_loop_add_signal(pw_main_loop_get_loop(loop), SIGTERM, do_quit, this); 299 | 300 | context = pw_context_new(pw_main_loop_get_loop(loop), nullptr, 0); 301 | if (context == nullptr) { 302 | throw std::runtime_error("Error creating Pipewire context"); 303 | } 304 | 305 | // Need this line or the profiler will not load. 306 | pw_context_load_module(context, PW_EXTENSION_MODULE_PROFILER, nullptr, nullptr); 307 | 308 | //TODO remote should be? (empty string seems to work correctly) 309 | const char *remote = ""; 310 | core = pw_context_connect(context, 311 | pw_properties_new(PW_KEY_REMOTE_NAME, remote, nullptr), 312 | 0); 313 | if (core == nullptr) { 314 | throw std::runtime_error("Can't connect to pipewire"); 315 | } 316 | 317 | pw_core_add_listener(core, 318 | &core_listener, 319 | &core_events, 320 | this); 321 | 322 | registry = pw_core_get_registry(core, 323 | PW_VERSION_REGISTRY, 324 | 0); 325 | pw_registry_add_listener(registry, 326 | ®istry_listener, 327 | ®istry_events, 328 | this); 329 | 330 | pipewire_media_session = new SystemdService(QStringLiteral("pipewire-media-session"), true); 331 | wireplumber_service = new SystemdService(QStringLiteral("wireplumber"), true); 332 | 333 | // QT stuff 334 | connect(m_nodes, &QPipewireNodeListModel::layoutChanged, this, [this]() { 335 | Q_EMIT nodesChanged(); 336 | }); 337 | } 338 | 339 | QPipewire::~QPipewire() 340 | { 341 | delete pipewire_media_session; 342 | 343 | if (pw_settings != nullptr) { 344 | delete pw_settings; 345 | } 346 | for (QPipewireClient* pw_client: pw_clients) { 347 | delete pw_client; 348 | } 349 | if (alsa_properties != nullptr) { 350 | delete alsa_properties; 351 | } 352 | if (pw_profiler != nullptr) { 353 | delete pw_profiler; 354 | } 355 | for(const QPipewireNode *node: m_nodes->list()) { 356 | delete node; 357 | } 358 | delete m_nodes; 359 | pw_proxy_destroy((struct pw_proxy*) registry); 360 | pw_core_disconnect(core); 361 | pw_context_destroy(context); 362 | pw_main_loop_destroy(loop); 363 | pw_deinit(); 364 | } 365 | -------------------------------------------------------------------------------- /src/pw/qpipewire.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #pragma once 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | #include 27 | 28 | class QPipewireMetadata; 29 | class QPipewireSettings; 30 | class QPipewireLink; 31 | class QPipewirePort; 32 | class QPipewireDevice; 33 | class QPipewireClient; 34 | class AlsaProperties; 35 | 36 | #include "src/pw/qpipewiremetadata.h" 37 | #include "src/pw/qpipewiresettings.h" 38 | #include "src/pw/qpipewirenode.h" 39 | #include "src/pw/qpipewireprofiler.h" 40 | #include "src/pw/qpipewirenodelistmodel.h" 41 | #include "src/pw/media-session/alsaproperties.h" 42 | #include "src/systemdservice.h" 43 | 44 | class QPipewire : public QObject 45 | { 46 | Q_OBJECT 47 | Q_PROPERTY(QString appVersion READ appVersion NOTIFY appVersionChanged) 48 | Q_PROPERTY(QPipewireSettings* settings READ settings NOTIFY settingsChanged) 49 | // Q_PROPERTY(QPipewireClient* client READ client NOTIFY clientChanged) 50 | Q_PROPERTY(QPipewireNodeListModel* nodes READ nodes NOTIFY nodesChanged) 51 | Q_PROPERTY(QList nodeList READ nodeList NOTIFY nodesChanged) 52 | Q_PROPERTY(QList linkList READ linkList NOTIFY linksChanged) 53 | Q_PROPERTY(QList portList READ portList NOTIFY portsChanged) 54 | Q_PROPERTY(QPipewireProfiler* profiler READ profiler NOTIFY profilerChanged) 55 | Q_PROPERTY(SystemdService* pipewireMediaSession READ pipewireMediaSession NOTIFY pipewireMediaSessionChanged) 56 | Q_PROPERTY(SystemdService* wirePlumberService READ wirePlumberService NOTIFY wirePlumberServiceChanged) 57 | Q_PROPERTY(AlsaProperties* alsaProperties READ alsaProperties NOTIFY alsaPropertiesChanged) 58 | 59 | Q_SIGNALS: 60 | void quit(); 61 | void registryObject(uint32_t id, 62 | uint32_t permissions, 63 | const char *type, 64 | uint32_t version, 65 | const struct spa_dict *props); 66 | 67 | void appVersionChanged(); //bogus, never Q_EMITted 68 | void settingsChanged(); 69 | // void clientChanged(); 70 | void nodesChanged(); 71 | void linksChanged(); 72 | void portsChanged(); 73 | void profilerChanged(); 74 | void pipewireMediaSessionChanged(); 75 | void wirePlumberServiceChanged(); 76 | void alsaPropertiesChanged(); 77 | 78 | private: 79 | struct pw_main_loop *loop = nullptr; 80 | 81 | struct pw_context *context = nullptr; 82 | 83 | struct pw_core *core = nullptr; 84 | struct spa_hook core_listener; 85 | 86 | struct pw_registry *registry = nullptr; 87 | struct spa_hook registry_listener; 88 | 89 | int sync = 0; 90 | bool round_trip_done = false; 91 | 92 | std::vector pw_clients = {}; 93 | QPipewireSettings *pw_settings = nullptr; 94 | QPipewireNodeListModel *m_nodes = nullptr; 95 | QList m_links; 96 | QList m_ports; 97 | QList m_devices; 98 | QPipewireProfiler *pw_profiler = nullptr; 99 | SystemdService *pipewire_media_session = nullptr; 100 | SystemdService *wireplumber_service = nullptr; 101 | AlsaProperties *alsa_properties = nullptr; 102 | 103 | public: 104 | explicit QPipewire(int *argc, char **argv[], QObject *parent = nullptr); 105 | virtual ~QPipewire(); 106 | 107 | /** 108 | * Executes pipewire main loop, calling callbacks and other stuff. 109 | * Runs in main thread and is blocking until all operations are done. 110 | */ 111 | Q_INVOKABLE void round_trip(); 112 | 113 | QString appVersion() { return QStringLiteral(PROJECT_VERSION); } 114 | Q_INVOKABLE QString pipewireCompiledVersion() { return QString::fromUtf8(pw_get_headers_version()); } 115 | Q_INVOKABLE QString pipewireLinkedVersion() { return QString::fromUtf8(pw_get_library_version()); } 116 | Q_INVOKABLE QString platformName() { return QGuiApplication::platformName(); } 117 | Q_INVOKABLE QString qtCompiledVersion() { return QStringLiteral(QT_VERSION_STR); } 118 | Q_INVOKABLE QString qtLinkedVersion() { return QString::fromLocal8Bit(qVersion()); } 119 | Q_INVOKABLE QString kframeworksCompiledVersion() { return QStringLiteral(KF6_COMPILED_VERSION); } 120 | 121 | Q_INVOKABLE static QString formatTime(double val); 122 | Q_INVOKABLE bool isPipewireMediaSession() { 123 | //Wireplumber may mask itself as pipewire-media-session, maybe not but let's be double sure 124 | return pipewire_media_session->running() && !wireplumber_service->running(); 125 | } 126 | Q_INVOKABLE bool isWireplumber() { 127 | return wireplumber_service->running(); 128 | } 129 | // QPipewireClient* client() { return pw_client; } 130 | QPipewireSettings* settings() { return pw_settings; } 131 | QPipewireProfiler* profiler() { return pw_profiler; } 132 | QPipewireNodeListModel* nodes() { return m_nodes; } 133 | QList nodeList() { return m_nodes->list(); } 134 | QList linkList() { return m_links; } 135 | QList portList() { return m_ports; } 136 | SystemdService* pipewireMediaSession() { return pipewire_media_session; } 137 | SystemdService* wirePlumberService() { return wireplumber_service; } 138 | AlsaProperties* alsaProperties() { return alsa_properties; } 139 | QObjectList nodeObjectList() { 140 | auto list = QObjectList(); 141 | for(int i=0; isize(); i++) { 142 | list.append(m_nodes->list()[i]); 143 | } 144 | return list; 145 | } 146 | 147 | public: // actually, private 148 | // PIPEWIRES CALLBACKS (private) 149 | void _quit(); 150 | void _on_core_done(uint32_t id, int seq); 151 | void _on_core_error(uint32_t id, int seq, int res, const char *message); 152 | void _registry_event(uint32_t id, 153 | uint32_t permissions, 154 | const char *type, 155 | uint32_t version, 156 | const struct spa_dict *props); 157 | void _registry_event_remove(uint32_t id); 158 | 159 | private: 160 | void resync(); 161 | 162 | friend class QPipewireClient; 163 | friend class QPipewireMetadata; 164 | friend class QPipewireProfiler; 165 | friend class QPipewireNode; 166 | friend class QPipewireLink; 167 | friend class QPipewirePort; 168 | friend class QPipewireDevice; 169 | }; 170 | -------------------------------------------------------------------------------- /src/pw/qpipewirealsanode.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include 18 | #include 19 | #include "qpipewirealsanode.h" 20 | 21 | QPipewireAlsaNode::QPipewireAlsaNode(QPipewire *parent, uint32_t id, const struct spa_dict *props) 22 | : QPipewireNode(parent, id, props) 23 | { 24 | m_name = QString::fromUtf8(spa_dict_lookup(props, PW_KEY_NODE_NICK)); 25 | } 26 | -------------------------------------------------------------------------------- /src/pw/qpipewirealsanode.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #pragma once 18 | 19 | #include "qpipewirenode.h" 20 | 21 | class QPipewireAlsaNode : public QPipewireNode { 22 | Q_OBJECT 23 | public: 24 | QPipewireAlsaNode() = default; 25 | explicit QPipewireAlsaNode(QPipewire *parent, uint32_t id, const struct spa_dict *props); 26 | ~QPipewireAlsaNode() override = default; 27 | 28 | Q_INVOKABLE [[nodiscard]] bool isAlsa() const override { return true; } 29 | }; 30 | -------------------------------------------------------------------------------- /src/pw/qpipewireclient.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "qpipewireclient.h" 18 | 19 | #include "src/pw/qpipewire.h" 20 | 21 | #include 22 | #include 23 | 24 | #define QPIPEWIRE_CAST(x) QPipewireClient* _this = static_cast(x); 25 | 26 | //----------------------------------------------------------------------------- 27 | 28 | static void client_info(void *data, const pw_client_info *info) 29 | { 30 | QPIPEWIRE_CAST(data); 31 | _this->_client_info(info); 32 | } 33 | 34 | void QPipewireClient::_client_info(const struct pw_client_info *info) 35 | { 36 | const struct spa_dict_item *item; 37 | spa_dict_for_each(item, info->props) { 38 | const QString key = QString::fromUtf8(item->key); 39 | const QString value = QString::fromUtf8(item->value); 40 | m_properties[key] = value; 41 | // std::clog << "client_" << this->m_id << ":[" << key.toStdString() << "]=" << value.toStdString() << std::endl; 42 | Q_EMIT propertyChanged(key, value); 43 | } 44 | 45 | Q_EMIT propertiesChanged(); 46 | } 47 | 48 | static const pw_client_events client_events { 49 | .version = PW_VERSION_CLIENT_EVENTS, 50 | .info = client_info, 51 | }; 52 | 53 | //----------------------------------------------------------------------------- 54 | 55 | QPipewireClient::QPipewireClient(QPipewire *parent, uint32_t id, const spa_dict* props) 56 | : QObject(parent) 57 | , pipewire(parent) 58 | , m_id(id) 59 | { 60 | client = static_cast( 61 | pw_registry_bind(pipewire->registry, id, PW_TYPE_INTERFACE_Client, PW_VERSION_CLIENT, 0)); 62 | 63 | if (client == nullptr) { 64 | throw std::runtime_error("Error creating client proxy"); 65 | } 66 | 67 | pw_client_add_listener(client, 68 | &client_listener, 69 | &client_events, 70 | this); 71 | pipewire->resync(); 72 | } 73 | 74 | QPipewireClient::~QPipewireClient() 75 | { 76 | spa_hook_remove(&client_listener); 77 | if (client != nullptr) { 78 | pw_proxy_destroy((struct pw_proxy*) client); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/pw/qpipewireclient.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #pragma once 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | class QPipewire; 29 | 30 | class QPipewireClient : public QObject 31 | { 32 | public: 33 | 34 | Q_OBJECT 35 | Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged) 36 | Q_PROPERTY(int id READ id NOTIFY idChanged) 37 | Q_PROPERTY(QList propertiesList READ propertiesList NOTIFY propertiesChanged) 38 | 39 | Q_SIGNALS: 40 | void propertiesChanged(); 41 | void idChanged(); 42 | void propertyChanged(QString key, QString value); 43 | 44 | private: 45 | QPipewire *pipewire = nullptr; 46 | uint32_t m_id = 0; 47 | 48 | struct pw_client *client = nullptr; 49 | struct spa_hook client_listener; 50 | 51 | QMap m_properties; 52 | 53 | public: 54 | explicit QPipewireClient(QPipewire *parent, uint32_t id, const spa_dict* props); 55 | virtual ~QPipewireClient(); 56 | 57 | [[nodiscard]] int id() const { return m_id; } 58 | 59 | QString property(const QString &key) 60 | { 61 | return m_properties[key]; 62 | } 63 | 64 | bool has_property(const QString &key) 65 | { 66 | return m_properties.contains(key); 67 | } 68 | 69 | QVariantMap properties() 70 | { 71 | QVariantMap rval; 72 | QMap::iterator i = m_properties.begin(); 73 | while (i != m_properties.end()) { 74 | rval[i.key()] = QVariant::fromValue(i.value()); 75 | ++i; 76 | } 77 | return rval; 78 | } 79 | 80 | QList propertiesList() 81 | { 82 | QList rval; 83 | QMap::iterator i = m_properties.begin(); 84 | while (i != m_properties.end()) { 85 | QVariantMap value; 86 | value[QStringLiteral("name")] = i.key(); 87 | value[QStringLiteral("value")] = i.value(); 88 | rval.append(value); 89 | ++i; 90 | } 91 | return rval; 92 | } 93 | 94 | void _client_info(const struct pw_client_info *info); 95 | }; 96 | -------------------------------------------------------------------------------- /src/pw/qpipewiredevice.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | #include "qpipewiredevice.h" 19 | #include "qpipewire.h" 20 | #include 21 | #include 22 | 23 | #define QPIPEWIRE_CAST(x) QPipewireDevice* _this = static_cast(x); 24 | 25 | static void s_device_info (void *data, const struct pw_device_info *info) 26 | { 27 | QPIPEWIRE_CAST(data); 28 | _this->_device_info(info); 29 | } 30 | 31 | void QPipewireDevice::_device_info(const struct pw_device_info* info) 32 | { 33 | if (info->change_mask & PW_DEVICE_CHANGE_MASK_PROPS) { 34 | 35 | } 36 | if (info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS) { 37 | //TODO pw_node_enum_params(m_device, 0, SPA_PARAM_Route, 0, 0, nullptr); 38 | } 39 | } 40 | 41 | 42 | static void s_device_param (void *data, int seq, 43 | uint32_t id, uint32_t index, uint32_t next, 44 | const struct spa_pod *param) 45 | { 46 | QPIPEWIRE_CAST(data); 47 | _this->_device_param(seq, id, index, next, param); 48 | } 49 | 50 | void QPipewireDevice::_device_param(int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod* param) 51 | { 52 | // spa_debug_pod(2, nullptr, param); 53 | 54 | const spa_pod_prop* prop; 55 | 56 | prop = spa_pod_find_prop(param, nullptr, SPA_PARAM_ROUTE_props); 57 | if (prop == nullptr) 58 | return; 59 | const spa_pod *route_props = &prop->value; 60 | 61 | prop = spa_pod_find_prop(route_props, nullptr, SPA_PROP_channelVolumes); 62 | if (prop == nullptr) 63 | return; 64 | 65 | assert(spa_pod_is_array(&prop->value)); 66 | const spa_pod_array *volumes = (const spa_pod_array*) &prop->value; 67 | 68 | uint32_t n_channels = 0; 69 | uint32_t type = SPA_POD_ARRAY_VALUE_TYPE(volumes); 70 | assert(SPA_TYPE_Float == type); 71 | float *data = (float*) spa_pod_get_array((const spa_pod*) volumes, &n_channels); 72 | m_volume = data[0]; 73 | Q_EMIT volumeChanged(m_volume); 74 | std::clog << "New volume for device " << m_id << " is " << m_volume << std::endl; 75 | } 76 | 77 | 78 | static const pw_device_events device_events = { 79 | .version = PW_VERSION_CORE_EVENTS, 80 | .info = &s_device_info, 81 | .param = &s_device_param 82 | }; 83 | 84 | QPipewireDevice::QPipewireDevice(QPipewire* parent, uint32_t id, const spa_dict* props) 85 | : QObject(parent) 86 | , pipewire(parent) 87 | , m_id(id) 88 | { 89 | m_device = static_cast( 90 | pw_registry_bind(pipewire->registry, id, PW_TYPE_INTERFACE_Device, 91 | PW_VERSION_CLIENT, 0)); 92 | 93 | if (m_device == nullptr) { 94 | throw std::runtime_error("Error creating device proxy"); 95 | } 96 | 97 | pw_device_add_listener(m_device, &m_listener, &device_events, this); 98 | } 99 | 100 | QPipewireDevice::~QPipewireDevice() 101 | { 102 | if (m_device != nullptr) { 103 | pw_proxy_destroy((pw_proxy*) m_device); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/pw/qpipewiredevice.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | #ifndef QPIPEWIREDEVICE_H 19 | #define QPIPEWIREDEVICE_H 20 | 21 | #include 22 | #include 23 | 24 | struct pw_device; 25 | class QPipewire; 26 | 27 | /** 28 | * @todo write docs 29 | */ 30 | class QPipewireDevice : public QObject 31 | { 32 | Q_OBJECT 33 | Q_PROPERTY(float volume READ volume WRITE setVolume NOTIFY volumeChanged) 34 | 35 | Q_SIGNALS: 36 | void volumeChanged(float); 37 | 38 | public: 39 | /** 40 | * Default constructor 41 | */ 42 | QPipewireDevice(QPipewire *parent, uint32_t id, const struct spa_dict *props); 43 | 44 | /** 45 | * Destructor 46 | */ 47 | virtual ~QPipewireDevice(); 48 | 49 | void _device_info (const struct pw_device_info *info); 50 | 51 | void _device_param (int seq, uint32_t id, uint32_t index, uint32_t next, 52 | const struct spa_pod *param); 53 | 54 | // setters getters 55 | float volume() const { return m_volume; } 56 | void setVolume(float v) { m_volume = v; /*TODO*/ } 57 | 58 | private: 59 | const QPipewire* pipewire; 60 | const uint32_t m_id; 61 | pw_device* m_device = nullptr; 62 | spa_hook m_listener; 63 | 64 | float m_volume = 0; 65 | }; 66 | 67 | #endif // QPIPEWIREDEVICE_H 68 | -------------------------------------------------------------------------------- /src/pw/qpipewirelink.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "qpipewirelink.h" 18 | #include "src/pw/qpipewire.h" 19 | #include "src/pw/utils.h" 20 | 21 | QPipewireLink::QPipewireLink(QPipewire* parent, uint32_t id, const spa_dict* props) 22 | : QObject(parent) 23 | , pipewire(parent) 24 | , m_id(id) 25 | , m_input_port(spa_dict_get_u32(props, PW_KEY_LINK_INPUT_PORT)) 26 | , m_output_port(spa_dict_get_u32(props, PW_KEY_LINK_OUTPUT_PORT)) 27 | , m_input_node(spa_dict_get_u32(props, PW_KEY_LINK_INPUT_NODE)) 28 | , m_output_node(spa_dict_get_u32(props, PW_KEY_LINK_INPUT_NODE)) 29 | { 30 | link = static_cast( 31 | pw_registry_bind(pipewire->registry, id, PW_TYPE_INTERFACE_Link, PW_VERSION_CLIENT, 0)); 32 | 33 | if (link == nullptr) { 34 | throw std::runtime_error("Error creating link proxy"); 35 | } 36 | // qDebug() << "Adding link (" << id << ") with props:"; 37 | // const struct spa_dict_item *item; 38 | // spa_dict_for_each(item, props) { 39 | // qDebug() << '\t' << item->key << ":" << item->value; 40 | // } 41 | } 42 | 43 | QPipewireLink::~QPipewireLink() 44 | { 45 | if (link != nullptr) { 46 | pw_proxy_destroy((struct pw_proxy*) link); 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/pw/qpipewirelink.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #pragma once 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | class QPipewire; 24 | 25 | /** 26 | * @todo write docs 27 | */ 28 | class QPipewireLink : public QObject 29 | { 30 | Q_OBJECT 31 | Q_PROPERTY(int id READ id NOTIFY idChanged) 32 | Q_PROPERTY(int inputPort READ inputPort NOTIFY inputPortChanged) 33 | Q_PROPERTY(int outputPort READ outputPort NOTIFY outputPortChanged) 34 | Q_PROPERTY(int inputNode READ inputNode NOTIFY inputNodeChanged) 35 | Q_PROPERTY(int outputNode READ outputNode NOTIFY outputNodeChanged) 36 | 37 | 38 | Q_SIGNALS: 39 | void idChanged(); 40 | void inputPortChanged(); 41 | void outputPortChanged(); 42 | void inputNodeChanged(); 43 | void outputNodeChanged(); 44 | 45 | protected: 46 | QPipewire* pipewire = nullptr; 47 | const uint32_t m_id; 48 | const uint32_t m_input_port; 49 | const uint32_t m_output_port; 50 | const uint32_t m_input_node; 51 | const uint32_t m_output_node; 52 | pw_link* link = nullptr; 53 | 54 | public: 55 | /** 56 | * Default constructor 57 | */ 58 | QPipewireLink(QPipewire* parent, uint32_t id, const spa_dict* props); 59 | 60 | /** 61 | * Destructor 62 | */ 63 | ~QPipewireLink() override; 64 | 65 | [[nodiscard]] int id() const { return m_id; } 66 | [[nodiscard]] uint32_t id_u32() const { return m_id; } 67 | [[nodiscard]] uint32_t inputPort() const { return m_input_port; } 68 | [[nodiscard]] uint32_t outputPort() const { return m_output_port; } 69 | [[nodiscard]] uint32_t inputNode() const { return m_input_node; } 70 | [[nodiscard]] uint32_t outputNode() const { return m_output_node; } 71 | }; 72 | 73 | std::ostream& operator<< (std::ostream& out, const QPipewireLink& link); 74 | -------------------------------------------------------------------------------- /src/pw/qpipewiremetadata.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "qpipewiremetadata.h" 18 | 19 | #include 20 | #include "src/pw/qpipewire.h" 21 | 22 | #define QPIPEWIRE_CAST(x) QPipewireMetadata* _this = static_cast(x); 23 | 24 | //----------------------------------------------------------------------------- 25 | static int metadata_property(void *data, 26 | uint32_t id, 27 | const char* key, 28 | const char* type, 29 | const char* value) 30 | { 31 | QPIPEWIRE_CAST(data); 32 | return _this->_metadata_property(id, key, type, value); 33 | } 34 | 35 | int QPipewireMetadata::_metadata_property(uint32_t id, 36 | const char* key, 37 | const char* type, 38 | const char* value) 39 | { 40 | if (key == nullptr) { 41 | Q_EMIT onAllKeysRemoved(id); 42 | } else if (value == nullptr) { 43 | Q_EMIT onKeyRemoved(id, key); 44 | } else { 45 | Q_EMIT onKeyUpdated(id, key, type, value); 46 | } 47 | 48 | return 0; 49 | } 50 | 51 | static const pw_metadata_events metadata_events { 52 | .version = PW_VERSION_METADATA_EVENTS, 53 | .property = metadata_property, 54 | }; 55 | 56 | //----------------------------------------------------------------------------- 57 | 58 | QPipewireMetadata::QPipewireMetadata(QPipewire *parent, uint32_t id, const spa_dict* props) 59 | : QObject(parent) 60 | , pipewire(parent) 61 | { 62 | metadata = static_cast( 63 | pw_registry_bind(parent->registry, id, PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0)); 64 | pw_metadata_add_listener(metadata, 65 | &metadata_listener, 66 | &metadata_events, 67 | this); 68 | parent->resync(); 69 | } 70 | 71 | QPipewireMetadata::~QPipewireMetadata() 72 | { 73 | spa_hook_remove(&metadata_listener); 74 | if (metadata != nullptr) { 75 | pw_proxy_destroy((struct pw_proxy*) metadata); 76 | } 77 | } 78 | 79 | //----------------------------------------------------------------------------- 80 | 81 | void QPipewireMetadata::setProperty(const char *key, const char *value) 82 | { 83 | pw_metadata_set_property(metadata, 84 | 0, 85 | key, 86 | nullptr, 87 | value); 88 | pipewire->round_trip(); 89 | } 90 | -------------------------------------------------------------------------------- /src/pw/qpipewiremetadata.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #pragma once 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | 24 | class QPipewire; 25 | 26 | class QPipewireMetadata : public QObject 27 | { 28 | Q_OBJECT 29 | Q_SIGNALS: 30 | void onAllKeysRemoved(uint32_t id); 31 | void onKeyRemoved(uint32_t id, const char* key); 32 | void onKeyUpdated(uint32_t id, const char* key, const char* type, const char* value); 33 | 34 | 35 | protected: 36 | QPipewire *pipewire = nullptr; 37 | struct pw_metadata *metadata = nullptr; 38 | struct spa_hook metadata_listener; 39 | 40 | public: 41 | explicit QPipewireMetadata(QPipewire *parent, uint32_t id, const spa_dict* props); 42 | virtual ~QPipewireMetadata(); 43 | 44 | void setProperty(const char *key, const char *value); 45 | void clear(); 46 | 47 | int _metadata_property(uint32_t id, 48 | const char* key, 49 | const char* type, 50 | const char* value); 51 | }; 52 | -------------------------------------------------------------------------------- /src/pw/qpipewirenode.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "qpipewirenode.h" 18 | #include "src/pw/qpipewire.h" 19 | #include "src/pw/utils.h" 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | template 30 | T clamp(const T v, const T low, const T high) 31 | { 32 | if (v < low) 33 | return low; 34 | if (v > high) 35 | return high; 36 | return v; 37 | } 38 | 39 | #define QPIPEWIRE_CAST(x) QPipewireNode* _this = static_cast(x); 40 | 41 | void node_event_info(void *data, const pw_node_info *info) 42 | { 43 | QPIPEWIRE_CAST(data); 44 | _this->_node_event_info(info); 45 | } 46 | 47 | void QPipewireNode::_node_event_info(const pw_node_info* info) 48 | { 49 | // message when it's updated 50 | // qDebug() << "node_event_info: "; 51 | const struct spa_dict_item *item; 52 | 53 | if (info->change_mask == PW_NODE_CHANGE_MASK_ALL) { 54 | //TODO 55 | // qDebug() << "CHANGE ALL"; 56 | } 57 | if (info->change_mask & PW_NODE_CHANGE_MASK_INPUT_PORTS ) { 58 | //TODO 59 | // qDebug() << "CHANGE INPUT PORTS"; 60 | } 61 | if (info->change_mask & PW_NODE_CHANGE_MASK_OUTPUT_PORTS) { 62 | //TODO 63 | // qDebug() << "CHANGE OUTPUT PORTS"; 64 | } 65 | if (info->change_mask & PW_NODE_CHANGE_MASK_STATE) { 66 | m_state = info->state; 67 | Q_EMIT stateChanged(); 68 | // qDebug() << "CHANGE STATE"; 69 | } 70 | if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS) { 71 | spa_dict_for_each(item, info->props) { 72 | //volume is not here.... 73 | const QString key = QString::fromUtf8(item->key); 74 | const QString value = QString::fromUtf8(item->value); 75 | m_properties[key] = value; 76 | //qWarning() << "node_" << this->m_name << ":[" << key << "]=" << value; 77 | //Q_EMIT propertyChanged(key, value); 78 | } 79 | // qDebug() << "CHANGE PROPS"; 80 | } 81 | if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) { 82 | enumParams(); 83 | // qDebug() << "CHANGE PARAMS"; 84 | } 85 | 86 | // spa_dict_for_each(item, info->props) { 87 | // //volume is not here.... 88 | // const QString key = item->key; 89 | // const QString value = item->value; 90 | // // m_properties[key] = value; 91 | // qWarning() << "node_" << this->m_name << ":[" << key << "]=" << value; 92 | // //Q_EMIT propertyChanged(key, value); 93 | // } 94 | 95 | // for(uint32_t i=0; in_params; i++) { 96 | // spa_param_info *param = info->params+i; 97 | // QString id; 98 | // switch ((spa_param_type) param->id) { 99 | // case SPA_PARAM_Invalid: id = "SPA_PARAM_Invalid"; break; /**< invalid */ 100 | // case SPA_PARAM_PropInfo: id = "SPA_PARAM_PropInfo"; break; /**< property information as SPA_TYPE_OBJECT_PropInfo */ 101 | // case SPA_PARAM_Props: id = "SPA_PARAM_Props"; break; /**< properties as SPA_TYPE_OBJECT_Props */ 102 | // case SPA_PARAM_EnumFormat: id = "SPA_PARAM_EnumFormat"; break; /**< available formats as SPA_TYPE_OBJECT_Format */ 103 | // case SPA_PARAM_Format: id = "SPA_PARAM_Format"; break; /**< configured format as SPA_TYPE_OBJECT_Format */ 104 | // case SPA_PARAM_Buffers: id = "SPA_PARAM_Buffers"; break; /**< buffer configurations as SPA_TYPE_OBJECT_ParamBuffers*/ 105 | // case SPA_PARAM_Meta: id = "SPA_PARAM_Meta"; break; /**< allowed metadata for buffers as SPA_TYPE_OBJECT_ParamMeta*/ 106 | // case SPA_PARAM_IO: id = "SPA_PARAM_IO"; break; /**< configurable IO areas as SPA_TYPE_OBJECT_ParamIO */ 107 | // case SPA_PARAM_EnumProfile: id = "SPA_PARAM_EnumProfile"; break; /**< profile enumeration as SPA_TYPE_OBJECT_ParamProfile */ 108 | // case SPA_PARAM_Profile: id = "SPA_PARAM_Profile"; break; /**< profile configuration as SPA_TYPE_OBJECT_ParamProfile */ 109 | // case SPA_PARAM_EnumPortConfig: id = "SPA_PARAM_EnumPortConfig"; break; /**< port configuration enumeration as SPA_TYPE_OBJECT_ParamPortConfig */ 110 | // case SPA_PARAM_PortConfig: id = "SPA_PARAM_PortConfig"; break; /**< port configuration as SPA_TYPE_OBJECT_ParamPortConfig */ 111 | // case SPA_PARAM_EnumRoute: id = "SPA_PARAM_EnumRoute"; break; /**< routing enumeration as SPA_TYPE_OBJECT_ParamRoute */ 112 | // case SPA_PARAM_Route: id = "SPA_PARAM_Route"; break; /**< routing configuration as SPA_TYPE_OBJECT_ParamRoute */ 113 | // case SPA_PARAM_Control: id = "SPA_PARAM_Control"; break; /**< Control parameter, a SPA_TYPE_Sequence */ 114 | // case SPA_PARAM_Latency: id = "SPA_PARAM_Latency"; break; /**< latency reporting, a SPA_TYPE_OBJECT_ParamLatency */ 115 | // case SPA_PARAM_ProcessLatency: id = "SPA_PARAM_ProcessLatency"; break; /**< processing latency, a SPA_TYPE_OBJECT_ParamProcessLatency */ 116 | // default: id = "ERROR"; break; 117 | // } 118 | // QString flags; 119 | // if (param->flags & SPA_PARAM_INFO_SERIAL) { 120 | // /*< bit to signal update even when the read/write flags don't change */ 121 | // flags += "SERIAL"; 122 | // } 123 | // if (param->flags & SPA_PARAM_INFO_READ) { 124 | // flags += "_READ"; 125 | // } 126 | // if (param->flags & SPA_PARAM_INFO_WRITE) { 127 | // flags += "_WRITE"; 128 | // } 129 | // 130 | // qWarning() << "id: " << id << " -> " << flags << " <- " << param->user; 131 | // } 132 | 133 | // spa_debug_pod(2, NULL, info->props); 134 | // spa_debug_pod(2, NULL, info->param); 135 | 136 | } 137 | 138 | 139 | void event_param(void *data, 140 | int seq, 141 | uint32_t id, 142 | uint32_t index, 143 | uint32_t next, 144 | const spa_pod *param) 145 | { 146 | QPIPEWIRE_CAST(data); 147 | _this->_event_param(seq, id, index, next, param); 148 | 149 | //volume is here! 150 | } 151 | 152 | void QPipewireNode::_event_param(int seq, uint32_t id, uint32_t index, uint32_t next, const spa_pod* param) 153 | { 154 | // spa_debug_pod(2, nullptr, param); 155 | 156 | const spa_pod_prop *prop; 157 | const spa_pod_object *obj = (const struct spa_pod_object*)param; 158 | // SPA_POD_OBJECT_FOREACH(obj, prop) { 159 | // printf("prop key:%d\n", prop->key); 160 | // } 161 | 162 | prop = spa_pod_find_prop(param, nullptr, SPA_PROP_channelVolumes); 163 | if (prop != nullptr) { 164 | assert(spa_pod_is_array(&prop->value)); 165 | const spa_pod_array *volumes = (const spa_pod_array*) &prop->value; 166 | 167 | uint32_t n_channels = 0; 168 | uint32_t type = SPA_POD_ARRAY_VALUE_TYPE(volumes); 169 | assert(SPA_TYPE_Float == type); 170 | float *data = (float*) spa_pod_get_array((const spa_pod*) volumes, &n_channels); 171 | m_volume = data[0]; 172 | Q_EMIT volumeChanged(m_volume); 173 | } 174 | } 175 | 176 | 177 | static const struct pw_node_events node_events = { 178 | .version = PW_VERSION_NODE_EVENTS, 179 | .info = &node_event_info, 180 | .param = &event_param 181 | }; 182 | 183 | 184 | QPipewireNode::QPipewireNode(QPipewire *parent, uint32_t id, const struct spa_dict *props) 185 | : QObject(parent) 186 | , pipewire(parent) 187 | , m_id(id) 188 | , m_node_type(NodeTypeNone) 189 | , m_media_type(MediaTypeNone) 190 | , m_driver(this) 191 | 192 | { 193 | m_node_name = QString::fromUtf8(spa_dict_lookup(props, PW_KEY_NODE_NAME)); 194 | m_node_description = QString::fromUtf8(spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION)); 195 | 196 | // Find name 197 | const char* str; 198 | if ((str = spa_dict_lookup(props, PW_KEY_NODE_NICK)) == nullptr && 199 | (str = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION)) == nullptr && 200 | (str = spa_dict_lookup(props, PW_KEY_NODE_NAME)) == nullptr) 201 | { 202 | str = spa_dict_lookup(props, PW_KEY_APP_NAME); 203 | } 204 | 205 | if (str == nullptr) { 206 | m_name = QString::number(id); 207 | } else { 208 | m_name = QString::fromUtf8(str); 209 | } 210 | 211 | // Find node type 212 | if ((str = spa_dict_lookup(props, PW_KEY_MEDIA_CATEGORY)) != nullptr) { 213 | m_category = QString::fromUtf8(str); 214 | } else { 215 | m_category = QStringLiteral(""); 216 | } 217 | 218 | // Find node type 219 | if ((str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS)) != nullptr) { 220 | m_media_class = QString::fromUtf8(str); 221 | 222 | if (m_media_class.contains(QStringLiteral("Audio"))) 223 | m_media_type = MediaTypeAudio; 224 | else if (m_media_class.contains(QStringLiteral("Video"))) 225 | m_media_type = MediaTypeVideo; 226 | else if (m_media_class.contains(QStringLiteral("Midi"))) 227 | m_media_type = MediaTypeMidi; 228 | else 229 | m_media_type = MediaTypeNone; 230 | } else { 231 | m_media_class = QStringLiteral(""); 232 | } 233 | 234 | if ((!m_category.isEmpty()) && m_category.contains(QStringLiteral("Duplex"))) { 235 | m_node_type = NodeTypeNone; 236 | } else if (!m_media_class.isEmpty()) { 237 | if (m_media_class.contains(QStringLiteral("Sink"))) { 238 | m_node_type = NodeTypeSink; 239 | } else if (m_media_class.contains(QStringLiteral("Input"))) { 240 | m_node_type = NodeTypeInput; 241 | } else if (m_media_class.contains(QStringLiteral("Source"))) { 242 | m_node_type = NodeTypeSource; 243 | } else if (m_media_class.contains(QStringLiteral("Output"))) { 244 | m_node_type = NodeTypeOutput; 245 | } 246 | } else { 247 | m_node_type = NodeTypeNone; 248 | } 249 | 250 | m_pw_node = static_cast( 251 | pw_registry_bind(pipewire->registry, m_id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0)); 252 | 253 | const struct spa_dict_item *item; 254 | spa_dict_for_each(item, props) { 255 | // qDebug() << "Property[" << item->key << "] =" << item->value; 256 | // std::cout << "Property[" << item->key << "]=" << item->value << std::endl; 257 | m_properties[QString::fromUtf8(item->key)] = QString::fromUtf8(item->value); 258 | } 259 | 260 | // struct spa_pod_object *obj = (struct spa_pod_object *) m_pw_node; 261 | // struct spa_pod_prop *prop; 262 | // SPA_POD_OBJECT_FOREACH(obj, prop) { 263 | // switch (prop->key) { 264 | // case SPA_PROP_volume: 265 | // float volume; 266 | // if (spa_pod_get_float(&prop->value, &volume) < 0) 267 | // continue; 268 | // if (volume == m_volume) 269 | // continue; 270 | // m_volume = volume; 271 | // Q_EMIT volumeChanged(m_volume); 272 | // break; 273 | // } 274 | // const spa_pod *pod = &prop->value; 275 | // std::cout << "KEY: " << prop->key << "=" << prop->value << std::endl; 276 | // } 277 | pw_proxy_add_object_listener((pw_proxy*) this->m_pw_node, &object_listener, &node_events, this); 278 | // enumParams(); 279 | } 280 | 281 | QPipewireNode::~QPipewireNode() 282 | { 283 | if(m_pw_node != nullptr) { 284 | pw_proxy_destroy((pw_proxy *) m_pw_node); 285 | } 286 | } 287 | 288 | QString QPipewireNode::formatPercentage(float val, float quantum) const 289 | { 290 | QString buf; 291 | buf.asprintf("%5.2f", quantum == 0.0f ? 0.0f : val/quantum); 292 | return buf; 293 | } 294 | 295 | QIcon QPipewireNode::activeIcon(bool active) const 296 | { 297 | if(active) { 298 | return QIcon::fromTheme(QStringLiteral("media-playback-start")); 299 | } else { 300 | return QIcon::fromTheme(QStringLiteral("media-playback-pause")); 301 | } 302 | } 303 | 304 | void QPipewireNode::setDriver(QPipewireNode* newDriver) 305 | { 306 | if (newDriver == m_driver) return; 307 | m_driver = newDriver; 308 | Q_EMIT driverChanged(); 309 | } 310 | 311 | void QPipewireNode::setMeasurement(const struct QPipewireNode::measurement &measure) 312 | { 313 | const struct measurement old = this->measurement; 314 | this->measurement = measure; 315 | if (old.status != measure.status) Q_EMIT activeChanged(); 316 | if (old.signal != measure.signal || old.awake != measure.awake) 317 | Q_EMIT waitingChanged(); 318 | if (old.finish != measure.finish || old.awake != measure.awake) 319 | Q_EMIT busyChanged(); 320 | if (m_driver != this && old.latency.num != measure.latency.num) 321 | Q_EMIT quantumChanged(); 322 | if (m_driver != this && old.latency.denom != measure.latency.denom) 323 | Q_EMIT rateChanged(); 324 | } 325 | 326 | void QPipewireNode::setInfo(const struct QPipewireNode::driver &info) 327 | { 328 | const struct driver old = this->info; 329 | this->info = info; 330 | if (m_driver == this && 331 | (old.clock.duration != info.clock.duration || old.clock.rate.num != info.clock.rate.num)) 332 | Q_EMIT quantumChanged(); 333 | if (m_driver == this && old.clock.rate.denom != info.clock.rate.denom) 334 | Q_EMIT rateChanged(); 335 | if (old.xrun_count != info.xrun_count) 336 | Q_EMIT xrunChanged(); 337 | } 338 | 339 | QVariant QPipewireNode::property(const char *key) 340 | { 341 | return m_properties[QString::fromUtf8(key)]; 342 | } 343 | 344 | void QPipewireNode::setProperty(const char *key, QVariant value) 345 | { 346 | throw std::runtime_error("not implemented"); 347 | } 348 | 349 | void QPipewireNode::setProperties(struct spa_pod *properties) 350 | { 351 | spa_debug_pod(0, nullptr, properties); 352 | int res = pw_node_set_param(m_pw_node, SPA_PARAM_Props, 0, properties); 353 | if (res < 0) { 354 | std::ostringstream err_msg; 355 | err_msg << "Got set_property error \"" << res << '"'; 356 | throw std::runtime_error(err_msg.str()); 357 | } 358 | } 359 | 360 | void QPipewireNode::setVolume(float volume) 361 | { 362 | volume = clamp(volume, 0.0f, 1.0f); 363 | struct spa_pod_builder builder = {}; 364 | unsigned char buffer[512]; 365 | spa_pod_builder_init(&builder, buffer, sizeof(buffer)); 366 | 367 | struct spa_pod *props = static_cast( 368 | spa_pod_builder_add_object(&builder, 369 | SPA_TYPE_OBJECT_Props, 0, 370 | SPA_PROP_volume, SPA_POD_Float(volume))); 371 | 372 | 373 | this->setProperties(props); 374 | 375 | m_volume = volume; 376 | Q_EMIT volumeChanged(m_volume); 377 | } 378 | 379 | 380 | void QPipewireNode::enumParams() 381 | // static bool do_enum_params(struct data *data, const char *cmd, char *args, char **error) 382 | { 383 | spa_pod *filter = nullptr; 384 | pw_node_enum_params(m_pw_node, _props_seq++, SPA_PARAM_Props, 0, 0, filter); 385 | // pw_node_enum_params(m_pw_node, _props_seq++, SPA_PARAM_Route, 0, 0, filter); 386 | // pw_node_set_param(); 387 | } 388 | 389 | -------------------------------------------------------------------------------- /src/pw/qpipewirenode.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #pragma once 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | class QPipewire; 29 | class QPipewireProfiler; 30 | 31 | class QPipewireNode : public QObject 32 | { 33 | public: 34 | enum NodeType { 35 | NodeTypeNone = 0, 36 | NodeTypeInput, 37 | NodeTypeOutput, 38 | NodeTypeSink, 39 | NodeTypeSource, 40 | }; 41 | 42 | enum MediaType { 43 | MediaTypeNone = 0, 44 | MediaTypeAudio, 45 | MediaTypeVideo, 46 | MediaTypeMidi, 47 | }; 48 | 49 | Q_OBJECT 50 | Q_ENUM(NodeType); 51 | Q_ENUM(MediaType); 52 | Q_PROPERTY(int id READ id NOTIFY idChanged) 53 | Q_PROPERTY(QString name READ name NOTIFY nameChanged) 54 | Q_PROPERTY(QString nodeName READ nodeName NOTIFY nodeNameChanged) 55 | Q_PROPERTY(QString nodeDescription READ nodeDescription NOTIFY nodeDescriptionChanged) 56 | Q_PROPERTY(QString category READ category NOTIFY categoryChanged) 57 | Q_PROPERTY(QString mediaClass READ mediaClass NOTIFY mediaClassChanged) 58 | Q_PROPERTY(NodeType nodeType READ nodeType NOTIFY nodeTypeChanged) 59 | Q_PROPERTY(MediaType mediaType READ mediaType NOTIFY mediaTypeChanged) 60 | Q_PROPERTY(QPipewireNode* driver READ driver NOTIFY driverChanged) 61 | Q_PROPERTY(bool active READ active NOTIFY activeChanged) 62 | Q_PROPERTY(double waiting READ waiting NOTIFY waitingChanged) 63 | Q_PROPERTY(double busy READ busy NOTIFY busyChanged) 64 | Q_PROPERTY(int quantum READ quantum NOTIFY quantumChanged) 65 | Q_PROPERTY(int rate READ rate NOTIFY rateChanged) 66 | Q_PROPERTY(int error READ error NOTIFY errorChanged) 67 | Q_PROPERTY(int xrun READ xrun NOTIFY xrunChanged) 68 | Q_PROPERTY(float volume READ volume WRITE setVolume NOTIFY volumeChanged) 69 | Q_PROPERTY(QString state READ state NOTIFY stateChanged) 70 | 71 | Q_SIGNALS: 72 | void idChanged(); 73 | void nameChanged(); 74 | void nodeNameChanged(); 75 | void nodeDescriptionChanged(); 76 | void categoryChanged(); 77 | void mediaClassChanged(); 78 | void nodeTypeChanged(); 79 | void mediaTypeChanged(); 80 | void driverChanged(); 81 | void activeChanged(); 82 | void waitingChanged(); 83 | void busyChanged(); 84 | void quantumChanged(); 85 | void rateChanged(); 86 | void errorChanged(); 87 | void xrunChanged(); 88 | void volumeChanged(float); 89 | void stateChanged(); 90 | 91 | protected: 92 | QPipewire *pipewire = nullptr; 93 | 94 | uint32_t m_id = 0; 95 | pw_node_state m_state = pw_node_state::PW_NODE_STATE_CREATING; 96 | QString m_name; 97 | QString m_node_name; 98 | QString m_node_description; 99 | QString m_category; 100 | QString m_media_class; 101 | NodeType m_node_type = NodeTypeNone; 102 | MediaType m_media_type = MediaTypeNone; 103 | struct pw_node *m_pw_node = nullptr; 104 | struct spa_node_info m_spa_node_info {}; 105 | QHash m_properties; 106 | spa_hook object_listener; 107 | uint32_t _props_seq = 0; 108 | 109 | 110 | struct measurement { 111 | int32_t index = 0; 112 | int32_t status = 0; 113 | int64_t quantum = 0; 114 | int64_t prev_signal = 0; 115 | int64_t signal = 0; 116 | int64_t awake = 0; 117 | int64_t finish = 0; 118 | struct spa_fraction latency = {0,0}; 119 | } measurement; 120 | 121 | struct driver { 122 | int64_t count = 0; 123 | float cpu_load[3] = {0,0,0}; 124 | struct spa_io_clock clock = {}; 125 | uint32_t xrun_count = 0; 126 | } info; 127 | 128 | QPipewireNode *m_driver = nullptr; 129 | uint32_t errors = 0; 130 | int32_t last_error_status = 0; 131 | 132 | float m_volume = 0.0; 133 | 134 | public: 135 | QPipewireNode() = default; 136 | explicit QPipewireNode(QPipewire *parent, uint32_t id, const struct spa_dict *props); 137 | virtual ~QPipewireNode(); 138 | 139 | void _node_event_info(const pw_node_info *info); 140 | void _event_param(int seq, uint32_t id, uint32_t index, uint32_t next, const spa_pod *param); 141 | 142 | int id() const { return m_id; } 143 | uint32_t id_u32() const { return m_id; } 144 | QString name() const { return m_name; } 145 | QString nodeName() const { return m_node_name; } 146 | QString nodeDescription() const { return m_node_description; } 147 | QString category() const { return m_category; } 148 | QString mediaClass() const { return m_media_class; } 149 | NodeType nodeType() const { return m_node_type; } 150 | MediaType mediaType() const { return m_media_type; } 151 | QPipewireNode *driver() { return m_driver != this ? m_driver : nullptr; } 152 | bool active() const { return measurement.status == 3; } 153 | double waiting() const { return (measurement.awake - measurement.signal) / 1000000000.f; } 154 | double busy() const { return (measurement.finish - measurement.awake) / 1000000000.f; } 155 | int quantum() const { 156 | if (m_driver == this) { 157 | return info.clock.duration * info.clock.rate.num; 158 | } else { 159 | return measurement.latency.num; 160 | } 161 | } 162 | int rate() const { 163 | if (m_driver == this) { 164 | return info.clock.rate.denom; 165 | } else { 166 | return measurement.latency.denom; 167 | } 168 | } 169 | int error() const { return errors; } 170 | int xrun() const { return info.xrun_count; } 171 | float volume() const { return m_volume; } 172 | QString state() const { 173 | switch (m_state) { 174 | case PW_NODE_STATE_ERROR: return QStringLiteral("ERROR"); 175 | case PW_NODE_STATE_CREATING: return QStringLiteral("CREATING"); 176 | case PW_NODE_STATE_SUSPENDED: return QStringLiteral("SUSPENDED"); 177 | case PW_NODE_STATE_IDLE: return QStringLiteral("IDLE"); 178 | case PW_NODE_STATE_RUNNING: return QStringLiteral("RUNNING"); 179 | default: return QStringLiteral("UNKNOWN"); 180 | } 181 | } 182 | 183 | Q_INVOKABLE QString formatPercentage(float val, float quantum) const; 184 | Q_INVOKABLE QIcon activeIcon(bool active) const; 185 | 186 | Q_INVOKABLE virtual bool isAlsa() const { return false; } 187 | 188 | /// Volume has to be between 0.0 and 1.0 (included) 189 | Q_INVOKABLE void setVolume(float volume); 190 | 191 | protected: 192 | 193 | float _quantum() 194 | { 195 | if (info.clock.rate.denom) 196 | return info.clock.duration * info.clock.rate.num / info.clock.rate.denom; 197 | else 198 | return 0; 199 | } 200 | 201 | void setDriver(QPipewireNode *newDriver); 202 | void setMeasurement(const struct measurement &measure); 203 | void setInfo(const struct driver &info); 204 | 205 | Q_INVOKABLE QVariant property(const char* key); 206 | Q_INVOKABLE void setProperty(const char* key, QVariant value); 207 | void setProperties(struct spa_pod *properties); 208 | void enumParams(); 209 | 210 | friend class QPipewireProfiler; 211 | }; 212 | 213 | Q_DECLARE_METATYPE(QPipewireNode*); 214 | -------------------------------------------------------------------------------- /src/pw/qpipewirenodelistmodel.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "qpipewirenodelistmodel.h" 18 | #include 19 | 20 | QPipewireNodeListModel::QPipewireNodeListModel(QObject *parent) 21 | : QAbstractListModel(parent) 22 | {} 23 | 24 | QPipewireNodeListModel::~QPipewireNodeListModel() 25 | {} 26 | 27 | void QPipewireNodeListModel::sortList() 28 | { 29 | Q_EMIT layoutAboutToBeChanged(); 30 | QList old = m_nodes; 31 | // std::sort(old.begin(), old.end(), [](QPipewireNode*a, QPipewireNode*b) { 32 | // return a->nodeName() > b->nodeName(); 33 | // }); 34 | m_nodes.clear(); 35 | for (QPipewireNode* device : old) { 36 | if (device->driver() == nullptr) { 37 | m_nodes.append(device); 38 | for(QPipewireNode* client : old) { 39 | if (device == client->driver()) { 40 | m_nodes.append(client); 41 | } 42 | } 43 | } 44 | } 45 | 46 | Q_EMIT layoutChanged(); 47 | } 48 | 49 | #include 50 | void QPipewireNodeListModel::append(QPipewireNode* node) 51 | { 52 | // qWarning() << "Adding new node " << node->name(); 53 | beginInsertRows(QModelIndex(), m_nodes.size(), m_nodes.size()); 54 | node->connect(node, &QPipewireNode::idChanged, this, [this, node]() {this->rowChanged(node, IDRole); }); 55 | node->connect(node, &QPipewireNode::activeChanged, this, [this, node]() {this->rowChanged(node, ActiveRole);}); 56 | node->connect(node, &QPipewireNode::rateChanged, this, [this, node]() {this->rowChanged(node, RateRole); }); 57 | node->connect(node, &QPipewireNode::quantumChanged, this, [this, node]() {this->rowChanged(node, QuantumRole); }); 58 | node->connect(node, &QPipewireNode::waitingChanged, this, [this, node]() {this->rowChanged(node, WaitRole); }); 59 | node->connect(node, &QPipewireNode::busyChanged, this, [this, node]() {this->rowChanged(node, BusyRole); }); 60 | node->connect(node, &QPipewireNode::nameChanged, this, [this, node]() {this->rowChanged(node, NameRole); }); 61 | node->connect(node, &QPipewireNode::driverChanged, this, [this, node]() {this->rowChanged(node, DriverIDRole);}); 62 | node->connect(node, &QPipewireNode::driverChanged, this, &QPipewireNodeListModel::sortList); 63 | m_nodes.append(node); 64 | endInsertRows(); 65 | sortList(); 66 | } 67 | 68 | bool QPipewireNodeListModel::removeOne(QPipewireNode* node) 69 | { 70 | // qWarning() << "Removing node" << node->name(); 71 | int index = m_nodes.indexOf(node); 72 | if (index != -1) { 73 | return removeAt(index); 74 | } else { 75 | return false; 76 | } 77 | } 78 | 79 | bool QPipewireNodeListModel::removeAt(int index) 80 | { 81 | if (index >= 0 && index < m_nodes.size()) { 82 | beginRemoveRows(QModelIndex(), index, index); 83 | m_nodes.removeAt(index); 84 | endRemoveRows(); 85 | return true; 86 | } else { 87 | return false; 88 | } 89 | } 90 | 91 | int QPipewireNodeListModel::rowCount(const QModelIndex &/*parent*/) const 92 | { 93 | return m_nodes.size(); 94 | } 95 | 96 | QVariant QPipewireNodeListModel::data(const QModelIndex &index, int _role) const 97 | { 98 | int role = NodeRoles(_role); 99 | int i = index.row(); 100 | QPipewireNode *node = m_nodes.at(i); 101 | switch (role) { 102 | case IndexRole: 103 | return i; 104 | case NodeRole: 105 | case Qt::DisplayRole: 106 | { 107 | if (node) { 108 | QVariant vNode; 109 | vNode.setValue(node); 110 | return vNode; 111 | } else { 112 | return QVariant(QVariant::Invalid); 113 | } 114 | } 115 | case IDRole: 116 | return node ? node->id() : QVariant(QStringLiteral("nullptr")); 117 | case ActiveRole: 118 | return node ? node->active() : false; 119 | case RateRole: 120 | return node ? node->rate() : QVariant(QStringLiteral("nullptr")); 121 | case QuantumRole: 122 | return node ? node->quantum() : QVariant(QStringLiteral("nullptr")); 123 | case WaitRole: 124 | return node ? node->waiting() : 0.0; 125 | case BusyRole: 126 | return node ? node->busy() : 0.0; 127 | case NameRole: 128 | return node ? node->name() : QVariant(QStringLiteral("[deleted]")); 129 | case DriverIDRole: 130 | if (node && node->driver()) { 131 | return node->driver()->id(); 132 | } else { 133 | return -1; 134 | } 135 | default: 136 | qWarning() << "UNDEFINED ROLE" << _role; 137 | throw std::runtime_error("Undefined role"); 138 | } 139 | } 140 | 141 | QVariant QPipewireNodeListModel::headerData(int section, Qt::Orientation orientation, int role) const 142 | { 143 | switch (role) { 144 | case IndexRole: 145 | return QStringLiteral("Index"); 146 | case NodeRole: 147 | case Qt::DisplayRole: 148 | return QStringLiteral("Node"); 149 | case IDRole: 150 | return QStringLiteral("ID"); 151 | case ActiveRole: 152 | return QStringLiteral("Active"); 153 | case RateRole: 154 | return QStringLiteral("Rate"); 155 | case QuantumRole: 156 | return QStringLiteral("Quantum"); 157 | case WaitRole: 158 | return QStringLiteral("Wait"); 159 | case BusyRole: 160 | return QStringLiteral("Busy"); 161 | case NameRole: 162 | return QStringLiteral("Name"); 163 | case DriverIDRole: 164 | return QStringLiteral("Driver"); 165 | default: 166 | qWarning() << "UNDEFINED ROLE" << role; 167 | throw std::runtime_error("Undefined role"); 168 | } 169 | } 170 | 171 | 172 | bool QPipewireNodeListModel::insertRows(int position, int count, const QModelIndex& parent) 173 | { 174 | beginInsertRows(parent, position, position+count-1); 175 | 176 | for(int i=0; i QPipewireNodeListModel::roleNames() const 197 | { 198 | return { 199 | { IndexRole, "index" }, 200 | { NodeRole, "node" }, 201 | { Qt::DisplayRole, "display"}, 202 | { IDRole, "id" }, 203 | { ActiveRole, "active" }, 204 | { RateRole, "rate" }, 205 | { QuantumRole, "quantum" }, 206 | { WaitRole, "wait" }, 207 | { BusyRole, "busy" }, 208 | { NameRole, "name" }, 209 | { DriverIDRole, "driverID" }, 210 | }; 211 | } 212 | 213 | void QPipewireNodeListModel::rowChanged(QPipewireNode* node, int role) 214 | { 215 | int index = m_nodes.indexOf(node); 216 | QModelIndex topLeft = createIndex(index, 0); 217 | QModelIndex bottomRight = createIndex(index, 0); 218 | Q_EMIT dataChanged(topLeft, bottomRight, {role}); 219 | } 220 | 221 | void QPipewireNodeListModel::move(int from, int to) 222 | { 223 | if (from == to) return; 224 | int n=1; 225 | if (from > to) { 226 | // Only move forwards - flip if backwards moving 227 | int tfrom = from; 228 | int tto = to; 229 | from = tto; 230 | to = tto+n; 231 | n = tfrom-tto; 232 | } 233 | 234 | beginMoveRows(QModelIndex(), from, from+n-1, QModelIndex(), to+n); 235 | for (int i=n-1; i>=0; i--) 236 | m_nodes.move(from+i, to); 237 | endMoveRows(); 238 | } 239 | 240 | -------------------------------------------------------------------------------- /src/pw/qpipewirenodelistmodel.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #pragma once 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include "src/pw/qpipewirenode.h" 24 | 25 | class QPipewireNodeListModel : public QAbstractListModel 26 | { 27 | Q_OBJECT 28 | 29 | enum NodeRoles { 30 | IndexRole = Qt::UserRole + 1, 31 | NodeRole, 32 | IDRole, 33 | ActiveRole, 34 | RateRole, 35 | QuantumRole, 36 | WaitRole, 37 | BusyRole, 38 | NameRole, 39 | DriverIDRole, 40 | }; 41 | const unsigned int N_ROLES = 10; 42 | 43 | private: 44 | QList m_nodes; 45 | 46 | public: 47 | explicit QPipewireNodeListModel(QObject *parent = nullptr); 48 | virtual ~QPipewireNodeListModel(); 49 | 50 | inline QList &list() { return m_nodes; } 51 | inline const QList &constList() const { return m_nodes; } 52 | 53 | Q_INVOKABLE 54 | void sortList(); 55 | 56 | public: 57 | // Delegate methods from m_nodes 58 | void append(QPipewireNode *node); 59 | bool removeOne(QPipewireNode *node); 60 | bool removeAt(int index); 61 | inline int size() const { return m_nodes.size(); } 62 | inline QPipewireNode*& operator[](int index) { return m_nodes[index]; } 63 | inline QPipewireNode* const& operator[](int index) const { return m_nodes[index]; } 64 | 65 | public: 66 | // QAbstractItemModel interface 67 | virtual int rowCount(const QModelIndex& index = QModelIndex()) const override; 68 | //virtual int columnCount(const QModelIndex& index = QModelIndex()) const override; 69 | virtual QVariant data(const QModelIndex& index, int role) const override; 70 | virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; 71 | virtual bool insertRows(int row, int count, const QModelIndex& parent) override; 72 | virtual bool removeRows(int row, int count, const QModelIndex& parent) override; 73 | virtual QHash roleNames() const override; 74 | 75 | void rowChanged(QPipewireNode* node, int role); 76 | 77 | Q_INVOKABLE 78 | void move(int oldIndex, int newIndex); 79 | }; 80 | -------------------------------------------------------------------------------- /src/pw/qpipewireport.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include 18 | #include "qpipewireport.h" 19 | #include "qpipewire.h" 20 | 21 | #define QPIPEWIRE_CAST(x) QPipewirePort* _this = static_cast(x); 22 | 23 | static void static_port_info(void *data, const struct pw_port_info *info) 24 | { 25 | QPIPEWIRE_CAST(data); 26 | _this->_port_info(info); 27 | } 28 | static void static_port_param(void *data, int seq, 29 | uint32_t id, uint32_t index, uint32_t next, 30 | const struct spa_pod *param) 31 | { 32 | QPIPEWIRE_CAST(data); 33 | _this->_port_param(seq, id, index, next, param); 34 | } 35 | 36 | static const pw_port_events port_events { 37 | .version = PW_VERSION_PORT_EVENTS, 38 | .info = static_port_info, 39 | .param = static_port_param, 40 | }; 41 | 42 | QPipewirePort::QPipewirePort(QPipewire* parent, uint32_t id, const spa_dict* props) 43 | : QObject(parent) 44 | , pipewire(parent) 45 | , m_id(id) 46 | { 47 | port = static_cast( 48 | pw_registry_bind(pipewire->registry, id, PW_TYPE_INTERFACE_Port, PW_VERSION_CLIENT, 0)); 49 | 50 | if (port == nullptr) { 51 | throw std::runtime_error("Error creating port proxy"); 52 | } 53 | 54 | pw_port_add_listener(port, 55 | &port_listener, 56 | &port_events, 57 | this); 58 | // qDebug() << "Adding port (" << id << ") with props:"; 59 | // const struct spa_dict_item *item; 60 | // spa_dict_for_each(item, props) { 61 | // qDebug() << '\t' << item->key << ":" << item->value; 62 | // } 63 | } 64 | 65 | QPipewirePort::~QPipewirePort() 66 | { 67 | spa_hook_remove(&port_listener); 68 | if (port != nullptr) { 69 | pw_proxy_destroy((struct pw_proxy*) port); 70 | } 71 | } 72 | 73 | uint32_t QPipewirePort::id() const 74 | { 75 | return m_id; 76 | } 77 | 78 | void QPipewirePort::set_id(uint32_t id) 79 | { 80 | if (m_id == id) { 81 | return; 82 | } 83 | 84 | m_id = id; 85 | Q_EMIT idChanged(m_id); 86 | } 87 | 88 | void QPipewirePort::_port_info(const struct pw_port_info *info) 89 | { 90 | // qDebug() << "Port info (" << this->m_id << "):"; 91 | // qDebug() << "\t change_mask:" << info->change_mask; 92 | // qDebug() << "\t direction:" << info->direction; 93 | // qDebug() << "\t id:" << info->id; 94 | // qDebug() << "\t n_params:" << info->n_params; 95 | // for(uint32_t i=0; in_params; i++) { 96 | // const spa_param_info* param = info->params+i; 97 | // qDebug() << "\t params("<id; 99 | // qDebug() << "\t\tflags:" << param->flags; 100 | // qDebug() << "\t\tuser:" << param->user; 101 | // } 102 | // qDebug() << "\t params_id:" << info->params->id; 103 | // qDebug() << "\t params_flags:" << info->params->flags; 104 | // qDebug() << "\t params_user:" << info->params->user; 105 | // qDebug() << "\t props:" << info->props; 106 | // const struct spa_dict_item *item; 107 | // spa_dict_for_each(item, info->props) { 108 | // qDebug() << "\t\t" << item->key << ":" << item->value; 109 | // } 110 | } 111 | 112 | void QPipewirePort::_port_param(int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param) 113 | { 114 | // qDebug() << "Port param (" << this->m_id << "):"; 115 | // qDebug() << "\t seq:" << seq; 116 | // qDebug() << "\t id:" << id; 117 | // qDebug() << "\t index:" << index; 118 | // qDebug() << "\t next:" << next; 119 | // qDebug() << "\t param:" << param; 120 | } 121 | -------------------------------------------------------------------------------- /src/pw/qpipewireport.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #pragma once 18 | 19 | #include 20 | #include "qpipewire.h" 21 | 22 | /** 23 | * @todo write docs 24 | */ 25 | class QPipewirePort : public QObject 26 | { 27 | Q_OBJECT 28 | Q_PROPERTY(uint32_t id READ id WRITE set_id NOTIFY idChanged) 29 | 30 | public: 31 | /** 32 | * Default constructor 33 | */ 34 | QPipewirePort(QPipewire *parent, uint32_t id, const spa_dict *props); 35 | 36 | /** 37 | * Destructor 38 | */ 39 | ~QPipewirePort() override; 40 | 41 | /** 42 | * @return the m_id 43 | */ 44 | uint32_t id() const; 45 | 46 | 47 | void _port_info(const struct pw_port_info *info); 48 | 49 | void _port_param(int seq, 50 | uint32_t id, uint32_t index, uint32_t next, 51 | const struct spa_pod *param); 52 | 53 | public Q_SLOTS: 54 | /** 55 | * Sets the id. 56 | * 57 | * @param id the new id 58 | */ 59 | void set_id(uint32_t id); 60 | 61 | Q_SIGNALS: 62 | void idChanged(uint32_t id); 63 | 64 | private: 65 | QPipewire* pipewire; 66 | uint32_t m_id = 0; 67 | pw_port *port = nullptr; 68 | spa_hook port_listener; 69 | }; 70 | -------------------------------------------------------------------------------- /src/pw/qpipewireprofiler.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "qpipewireprofiler.h" 18 | #include "src/pw/qpipewire.h" 19 | #include "src/pw/qpipewirenode.h" 20 | 21 | #include 22 | 23 | #define QPIPEWIRE_CAST(x) QPipewireProfiler* _this = static_cast(x); 24 | 25 | void profiler_profile(void *data, const spa_pod *pod) 26 | { 27 | QPIPEWIRE_CAST(data); 28 | _this->_profiler_profile(pod); 29 | } 30 | 31 | void QPipewireProfiler::_profiler_profile(const spa_pod *pod) 32 | { 33 | const spa_pod *obj = nullptr; 34 | 35 | // SPA_POD_STRUCT_FOREACH(pod, obj) 36 | uint32_t pod_body_size = SPA_POD_BODY_SIZE(pod); 37 | for (obj = static_cast(SPA_POD_BODY(pod)); 38 | spa_pod_is_inside(SPA_POD_BODY(pod), pod_body_size, obj); 39 | obj = static_cast(spa_pod_next(obj))) 40 | { 41 | int res = 0; 42 | if (!spa_pod_is_object_type(obj, SPA_TYPE_OBJECT_Profiler)) 43 | continue; 44 | 45 | const spa_pod_prop *property = nullptr; 46 | point point; 47 | spa_zero(point); 48 | SPA_POD_OBJECT_FOREACH((struct spa_pod_object*)obj, property) 49 | { 50 | switch(property->key) { 51 | case SPA_PROFILER_info: 52 | res = process_info(&property->value, &point.info); 53 | break; 54 | case SPA_PROFILER_clock: 55 | res = process_clock(&property->value, &point.info); 56 | break; 57 | case SPA_PROFILER_driverBlock: 58 | res = process_driver_block(&property->value, &point); 59 | break; 60 | case SPA_PROFILER_followerBlock: 61 | process_follower_block(&property->value, &point); 62 | break; 63 | default: 64 | break; 65 | } 66 | if (res < 0) 67 | break; 68 | } 69 | if (res < 0) 70 | continue; 71 | } 72 | } 73 | 74 | static const struct pw_profiler_events profiler_events = { 75 | .version = PW_VERSION_PROFILER_EVENTS, 76 | .profile = profiler_profile, 77 | }; 78 | 79 | // ---------------------------------------------------------------------------- 80 | 81 | int QPipewireProfiler::process_info(const struct spa_pod *pod, struct QPipewireNode::driver *info) 82 | { 83 | return spa_pod_parse_struct(pod, 84 | SPA_POD_Long(&info->count), 85 | SPA_POD_Float(&info->cpu_load[0]), 86 | SPA_POD_Float(&info->cpu_load[1]), 87 | SPA_POD_Float(&info->cpu_load[2]), 88 | SPA_POD_Int(&info->xrun_count)); 89 | } 90 | 91 | int QPipewireProfiler::process_clock(const struct spa_pod *pod, struct QPipewireNode::driver *info) 92 | { 93 | return spa_pod_parse_struct(pod, 94 | SPA_POD_Int(&info->clock.flags), 95 | SPA_POD_Int(&info->clock.id), 96 | SPA_POD_Stringn(info->clock.name, sizeof(info->clock.name)), 97 | SPA_POD_Long(&info->clock.nsec), 98 | SPA_POD_Fraction(&info->clock.rate), 99 | SPA_POD_Long(&info->clock.position), 100 | SPA_POD_Long(&info->clock.duration), 101 | SPA_POD_Long(&info->clock.delay), 102 | SPA_POD_Double(&info->clock.rate_diff), 103 | SPA_POD_Long(&info->clock.next_nsec)); 104 | } 105 | 106 | int QPipewireProfiler::process_driver_block(const struct spa_pod *pod, struct point *point) 107 | { 108 | char *name = NULL; 109 | uint32_t id = 0; 110 | struct QPipewireNode::measurement measure; 111 | QPipewireNode *node = nullptr; 112 | 113 | spa_zero(measure); 114 | int res = spa_pod_parse_struct(pod, 115 | SPA_POD_Int(&id), 116 | SPA_POD_String(&name), 117 | SPA_POD_Long(&measure.prev_signal), 118 | SPA_POD_Long(&measure.signal), 119 | SPA_POD_Long(&measure.awake), 120 | SPA_POD_Long(&measure.finish), 121 | SPA_POD_Int(&measure.status), 122 | SPA_POD_Fraction(&measure.latency)); 123 | if (res < 0) 124 | return res; 125 | 126 | for(QPipewireNode *i: pipewire->m_nodes->list()) { 127 | if (i && i->id() == id) { 128 | node = i; 129 | break; 130 | } 131 | } 132 | 133 | if (node == nullptr) 134 | return -ENOENT; 135 | 136 | node->setDriver(node); 137 | node->setMeasurement(measure); 138 | node->setInfo(point->info); 139 | point->driver = node; 140 | 141 | if (measure.status != 3) { 142 | node->errors++; 143 | Q_EMIT node->errorChanged(); 144 | if (node->last_error_status == -1) 145 | node->last_error_status = measure.status; 146 | } 147 | 148 | return 0; 149 | } 150 | 151 | int QPipewireProfiler::process_follower_block(const struct spa_pod *pod, struct point *point) 152 | { 153 | uint32_t id = 0; 154 | const char *name = nullptr; 155 | struct QPipewireNode::measurement measure; 156 | QPipewireNode *node = nullptr; 157 | 158 | spa_zero(measure); 159 | int res = spa_pod_parse_struct(pod, 160 | SPA_POD_Int(&id), 161 | SPA_POD_String(&name), 162 | SPA_POD_Long(&measure.prev_signal), 163 | SPA_POD_Long(&measure.signal), 164 | SPA_POD_Long(&measure.awake), 165 | SPA_POD_Long(&measure.finish), 166 | SPA_POD_Int(&measure.status), 167 | SPA_POD_Fraction(&measure.latency)); 168 | if (res < 0) 169 | return res; 170 | 171 | for (QPipewireNode *i: pipewire->m_nodes->list()) { 172 | if (i && i->id() == id) { 173 | node = i; 174 | break; 175 | } 176 | } 177 | 178 | if (node == nullptr) 179 | return -ENOENT; 180 | 181 | node->setMeasurement(measure); 182 | node->setDriver(point->driver); 183 | if (measure.status != 3) { 184 | node->errors++; 185 | Q_EMIT node->errorChanged(); 186 | if (node->last_error_status == -1) 187 | node->last_error_status = measure.status; 188 | } 189 | 190 | return 0; 191 | } 192 | 193 | // ---------------------------------------------------------------------------- 194 | QPipewireProfiler::QPipewireProfiler(QPipewire *parent, uint32_t id, const spa_dict* props) 195 | : QObject(parent) 196 | , pipewire(parent) 197 | { 198 | profiler = static_cast( 199 | pw_registry_bind(pipewire->registry, id, PW_TYPE_INTERFACE_Profiler, PW_VERSION_PROFILER, 0)); 200 | if (profiler == nullptr) { 201 | throw std::runtime_error("Error creating profiler proxy"); 202 | } 203 | 204 | pw_proxy_add_object_listener(profiler, 205 | &profiler_listener, 206 | &profiler_events, 207 | this); 208 | 209 | pipewire->resync(); 210 | } 211 | 212 | QPipewireProfiler::~QPipewireProfiler() 213 | { 214 | spa_hook_remove(&profiler_listener); 215 | pw_proxy_destroy(profiler); 216 | } 217 | -------------------------------------------------------------------------------- /src/pw/qpipewireprofiler.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #pragma once 18 | 19 | #include 20 | 21 | #include 22 | #include 23 | 24 | #include 25 | #include "src/pw/qpipewirenode.h" 26 | 27 | class QPipewire; 28 | 29 | class QPipewireProfiler : public QObject 30 | { 31 | Q_OBJECT 32 | Q_SIGNALS: 33 | 34 | private: 35 | QPipewire *pipewire = nullptr; 36 | pw_proxy *profiler = nullptr; 37 | spa_hook profiler_listener; 38 | int check_profiler; 39 | 40 | struct point { 41 | struct QPipewireNode *driver; 42 | struct QPipewireNode::driver info; 43 | }; 44 | 45 | public: 46 | explicit QPipewireProfiler(QPipewire *parent, uint32_t id, const spa_dict* props); 47 | virtual ~QPipewireProfiler(); 48 | 49 | int process_info(const struct spa_pod *pod, struct QPipewireNode::driver *info); 50 | int process_clock(const struct spa_pod *pod, struct QPipewireNode::driver *info); 51 | int process_driver_block(const struct spa_pod *pod, struct point *point); 52 | int process_follower_block(const struct spa_pod *pod, struct point *point); 53 | 54 | // private 55 | void _profiler_profile(const struct spa_pod *pod); 56 | }; 57 | -------------------------------------------------------------------------------- /src/pw/qpipewiresettings.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "qpipewiresettings.h" 18 | #include 19 | 20 | #define LOG_LEVEL_KEY "log.level" 21 | #define MIN_BUFFER_KEY "clock.min-quantum" 22 | #define MAX_BUFFER_KEY "clock.max-quantum" 23 | #define FORCE_SAMPLERATE_KEY "clock.force-rate" 24 | #define FORCE_BUFFER_KEY "clock.force-quantum" 25 | #define RATE_KEY "clock.rate" 26 | #define ALLOWED_RATES_KEY "clock.allowed-rates" 27 | #define QUANTUM_KEY "clock.quantum" 28 | 29 | //----------------------------------------------------------------------------- 30 | 31 | QPipewireSettings::QPipewireSettings(QPipewire *parent, uint32_t id, const spa_dict* props) 32 | :QPipewireMetadata(parent, id, props) 33 | { 34 | connect(this, &QPipewireMetadata::onKeyUpdated, this, &QPipewireSettings::keyUpdated); 35 | } 36 | 37 | //----------------------------------------------------------------------------- 38 | 39 | void QPipewireSettings::keyUpdated(uint32_t id, const char *key, const char *type, const char *value) 40 | { 41 | if (strcmp(key, LOG_LEVEL_KEY)==0){ 42 | m_logLevel = static_cast(std::stoi(value)); 43 | Q_EMIT logLevelChanged(m_logLevel); 44 | } else if (strcmp(key, FORCE_SAMPLERATE_KEY)==0){ 45 | m_force_sampleRate = std::stoi(value); 46 | Q_EMIT force_sampleRateChanged(m_force_sampleRate); 47 | } else if (strcmp(key, FORCE_BUFFER_KEY)==0) { 48 | m_force_buffer = std::stoi(value); 49 | Q_EMIT force_bufferChanged(m_force_buffer); 50 | } else if (strcmp(key, MIN_BUFFER_KEY)==0) { 51 | m_minBuffer = std::stoi(value); 52 | Q_EMIT minBufferChanged(m_minBuffer); 53 | } else if (strcmp(key, MAX_BUFFER_KEY)==0) { 54 | m_maxBuffer = std::stoi(value); 55 | Q_EMIT maxBufferChanged(m_maxBuffer); 56 | // } else if (strcmp(key, RATE_KEY)==0) { 57 | // //TODO 58 | // } else if (strcmp(key, ALLOWED_RATES_KEY)==0) { 59 | // //TODO 60 | // } else if (strcmp(key, QUANTUM_KEY)==0) { 61 | // //TODO 62 | } else { 63 | qWarning() << "Unrecognized pipewire setting \"" << key << "\" with value \"" << value << '"'; 64 | } 65 | } 66 | 67 | //----------------------------------------------------------------------------- 68 | 69 | void QPipewireSettings::setLogLevel(int newLogLevel) 70 | { 71 | if (newLogLevel == m_logLevel) return; 72 | std::string value = std::to_string(newLogLevel); 73 | this->setProperty(LOG_LEVEL_KEY, value.c_str()); 74 | } 75 | 76 | void QPipewireSettings::setMinBuffer(int newMinBuffer) 77 | { 78 | if (newMinBuffer == m_minBuffer) return; 79 | std::string value = std::to_string(newMinBuffer); 80 | this->setProperty(MIN_BUFFER_KEY, value.c_str()); 81 | } 82 | 83 | void QPipewireSettings::setMaxBuffer(int newMaxBuffer) 84 | { 85 | if (newMaxBuffer == m_maxBuffer) return; 86 | std::string value = std::to_string(newMaxBuffer); 87 | this->setProperty(MAX_BUFFER_KEY, value.c_str()); 88 | } 89 | 90 | void QPipewireSettings::setForce_sampleRate(int newSampleRate) 91 | { 92 | if (newSampleRate == m_force_sampleRate) return; 93 | std::string value = std::to_string(newSampleRate); 94 | this->setProperty(FORCE_SAMPLERATE_KEY, value.c_str()); 95 | } 96 | 97 | void QPipewireSettings::setForce_buffer(int newBuffer) 98 | { 99 | if (newBuffer == m_force_buffer) return; 100 | std::string value = std::to_string(newBuffer); 101 | this->setProperty(FORCE_BUFFER_KEY, value.c_str()); 102 | } 103 | -------------------------------------------------------------------------------- /src/pw/qpipewiresettings.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #pragma once 18 | 19 | #include 20 | 21 | class QPipewireMetadata; 22 | class QPipewire; 23 | 24 | #include "src/pw/qpipewiremetadata.h" 25 | #include 26 | 27 | class QPipewireSettings : public QPipewireMetadata 28 | { 29 | Q_OBJECT 30 | Q_PROPERTY(int logLevel READ logLevel WRITE setLogLevel NOTIFY logLevelChanged) 31 | Q_PROPERTY(int minBuffer READ minBuffer WRITE setMinBuffer NOTIFY minBufferChanged) 32 | Q_PROPERTY(int maxBuffer READ maxBuffer WRITE setMaxBuffer NOTIFY maxBufferChanged) 33 | Q_PROPERTY(int force_sampleRate READ force_sampleRate WRITE setForce_sampleRate NOTIFY force_sampleRateChanged) 34 | Q_PROPERTY(int force_buffer READ force_buffer WRITE setForce_buffer NOTIFY force_bufferChanged) 35 | 36 | Q_SIGNALS: 37 | void logLevelChanged(int); 38 | void force_sampleRateChanged(int); 39 | void force_bufferChanged(int); 40 | void minBufferChanged(int); 41 | void maxBufferChanged(int); 42 | 43 | private: 44 | int m_logLevel = static_cast(spa_log_level::SPA_LOG_LEVEL_WARN); 45 | int m_minBuffer = 0; 46 | int m_maxBuffer = 0; 47 | int m_force_sampleRate = 0; 48 | int m_force_buffer = 0; 49 | 50 | public: 51 | explicit QPipewireSettings(QPipewire *parent, uint32_t id, const spa_dict* props); 52 | virtual ~QPipewireSettings() = default; 53 | 54 | int logLevel() { return m_logLevel; } 55 | int minBuffer() { return m_minBuffer; } 56 | int maxBuffer() { return m_maxBuffer; } 57 | int force_sampleRate() { return m_force_sampleRate; } 58 | int force_buffer() { return m_force_buffer; } 59 | 60 | void setLogLevel(int newLogLevel); 61 | void setMinBuffer(int newMinBuffer); 62 | void setMaxBuffer(int newMaxBuffer); 63 | void setForce_sampleRate(int newSampleRate); 64 | void setForce_buffer(int newBuffer); 65 | 66 | private: 67 | void keyUpdated(uint32_t id, const char* key, const char* type, const char* value); 68 | }; 69 | -------------------------------------------------------------------------------- /src/pw/utils.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "utils.h" 18 | #include 19 | #include 20 | 21 | void print_dict(const struct spa_dict *dictionary) 22 | { 23 | const struct spa_dict_item *item; 24 | spa_dict_for_each(item, dictionary) { 25 | std::cout << '\t' << item->key << ":" << item->value << std::endl; 26 | } 27 | } 28 | 29 | uint32_t spa_dict_get_u32(const spa_dict *props, const char *key) 30 | { 31 | const char* str = spa_dict_lookup(props, key); 32 | if (str) { 33 | try { return std::stoul(str); } 34 | catch (const std::invalid_argument&) {} 35 | catch (const std::out_of_range&) {} 36 | } 37 | return -1; 38 | } 39 | 40 | std::ostream& operator<< (std::ostream& out, const spa_pod& _pod) 41 | { 42 | const spa_pod *pod = &_pod; 43 | switch (pod->type) { 44 | case SPA_TYPE_None: 45 | out << "[None]"; 46 | break; 47 | case SPA_TYPE_Bool: 48 | out << SPA_POD_VALUE(spa_pod_bool, pod); 49 | break; 50 | case SPA_TYPE_Id: 51 | out << SPA_POD_VALUE(spa_pod_id, pod); 52 | break; 53 | case SPA_TYPE_Int: 54 | out << SPA_POD_VALUE(spa_pod_int, pod); 55 | break; 56 | case SPA_TYPE_Long: 57 | out << SPA_POD_VALUE(spa_pod_long, pod); 58 | break; 59 | case SPA_TYPE_Float: 60 | out << SPA_POD_VALUE(spa_pod_float, pod); 61 | break; 62 | case SPA_TYPE_Double: 63 | out << SPA_POD_VALUE(spa_pod_double, pod); 64 | break; 65 | case SPA_TYPE_String: 66 | out << "[string]";//SPA_POD_VALUE(spa_pod_string, pod); 67 | break; 68 | case SPA_TYPE_Bytes: 69 | out << "[bytes]";//SPA_POD_VALUE(spa_type_, pod); 70 | break; 71 | case SPA_TYPE_Rectangle: 72 | out << "[rectangle]";//SPA_POD_VALUE(spa_type_rec, pod); 73 | break; 74 | case SPA_TYPE_Fraction: 75 | out << "[fraction]";//SPA_POD_VALUE(spa_type_fraction, pod); 76 | break; 77 | case SPA_TYPE_Bitmap: 78 | out << "[bitmap]";//SPA_POD_VALUE(spa_type_bitmap, pod); 79 | break; 80 | case SPA_TYPE_Array: 81 | out << "[array]";//SPA_POD_VALUE(spa_type_array, pod); 82 | break; 83 | case SPA_TYPE_Struct: 84 | out << "[struct]";//SPA_POD_VALUE(SPA_TYPE_, pod); 85 | break; 86 | case SPA_TYPE_Object: 87 | out << "[object]";//SPA_POD_VALUE(SPA_TYPE_, pod); 88 | break; 89 | case SPA_TYPE_Sequence: 90 | out << "[sequence]";//SPA_POD_VALUE(SPA_TYPE_, pod); 91 | break; 92 | case SPA_TYPE_Pointer: 93 | out << "[pointer]";//SPA_POD_VALUE(SPA_TYPE_, pod); 94 | break; 95 | case SPA_TYPE_Fd: 96 | out << "[fd]";//SPA_POD_VALUE(SPA_TYPE_, pod); 97 | break; 98 | case SPA_TYPE_Choice: 99 | out << "[choice]";//SPA_POD_VALUE(SPA_TYPE_, pod); 100 | break; 101 | case SPA_TYPE_Pod: 102 | out << "[pod]";//SPA_POD_VALUE(SPA_TYPE_, pod); 103 | break; 104 | default: 105 | out << "{unknown}"; 106 | break; 107 | } 108 | return out; 109 | } 110 | -------------------------------------------------------------------------------- /src/pw/utils.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #pragma once 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | void print_dict(const struct spa_dict *dictionary); 24 | 25 | uint32_t spa_dict_get_u32(const spa_dict *props, const char* key); 26 | 27 | std::ostream& operator<< (std::ostream& out, const spa_pod& pod); 28 | -------------------------------------------------------------------------------- /src/systemdservice.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "systemdservice.h" 18 | #include 19 | #include 20 | #include 21 | 22 | static bool argToString(const QDBusArgument &arg, QString &out); 23 | static bool variantToString(const QVariant &arg, QString &out); 24 | 25 | SystemdService::SystemdService(QString serviceName, bool userService) 26 | : serviceName(serviceName) 27 | , bus(userService ? QDBusConnection::sessionBus() : QDBusConnection::systemBus()) 28 | { 29 | if (!serviceName.endsWith(QStringLiteral(".service"))) { 30 | this->serviceName.append(QStringLiteral(".service")); 31 | } 32 | 33 | systemd = new QDBusInterface(QStringLiteral("org.freedesktop.systemd1"), 34 | QStringLiteral("/org/freedesktop/systemd1"), 35 | QStringLiteral("org.freedesktop.systemd1.Manager"), 36 | bus); 37 | 38 | checkIsRunning(); 39 | } 40 | 41 | SystemdService::~SystemdService() 42 | { 43 | delete systemd; 44 | } 45 | 46 | void SystemdService::setRunning(bool running) 47 | { 48 | if (isRunning == running) return; 49 | if (running) { 50 | this->start(); 51 | } else { 52 | this->stop(); 53 | } 54 | isRunning = running; 55 | Q_EMIT runningChanged(isRunning); 56 | } 57 | 58 | void SystemdService::_setRunning(bool running) 59 | { 60 | if (isRunning == running) return; 61 | isRunning = running; 62 | Q_EMIT runningChanged(isRunning); 63 | } 64 | 65 | bool SystemdService::checkIsRunning() 66 | { 67 | bool service_found = false; 68 | QDBusMessage r = systemd->call(QStringLiteral("ListUnits")); 69 | if (systemd->lastError().isValid()) 70 | qWarning() << QStringLiteral("Call failed: ") << systemd->lastError().message(); 71 | else { 72 | assert(r.signature() == QStringLiteral("a(ssssssouso)")); 73 | 74 | QVariantList arguments = r.arguments(); 75 | const QDBusArgument busArgument = qvariant_cast(arguments.at(0)); 76 | QString busSig = busArgument.currentSignature(); 77 | assert(busSig == r.signature()); 78 | QDBusArgument::ElementType elementType = busArgument.currentType(); 79 | assert(elementType == QDBusArgument::ArrayType); 80 | busArgument.beginArray(); 81 | while (!busArgument.atEnd()) 82 | { 83 | busArgument.beginStructure(); 84 | // The primary unit name as string 85 | QString name = busArgument.asVariant().toString(); 86 | 87 | if (name != this->serviceName) { 88 | // We are only interested in one service 89 | busArgument.endStructure(); 90 | continue; 91 | } 92 | // Service found! 93 | 94 | // The human readable description string 95 | QString desc = busArgument.asVariant().toString(); 96 | // The load state (i.e. whether the unit file has been loaded successfully) 97 | QString loaded = busArgument.asVariant().toString(); 98 | // The active state (i.e. whether the unit is currently started or not) 99 | QString active = busArgument.asVariant().toString(); 100 | // The sub state (a more fine-grained version of the active state that is specific to the unit type, which the active state is not) 101 | QString substate = busArgument.asVariant().toString(); 102 | // A unit that is being followed in its state by this unit, if there is any, otherwise the empty string. 103 | QString following = busArgument.asVariant().toString(); 104 | // The unit object path 105 | QString path = busArgument.asVariant().toString(); 106 | // If there is a job queued for the job unit the numeric job id, 0 otherwise 107 | uint jobqueued = busArgument.asVariant().toUInt(); 108 | // The job type as string 109 | QString jobtype = busArgument.asVariant().toString(); 110 | // The job object path 111 | QString jobobject = busArgument.asVariant().toString(); 112 | 113 | 114 | // std::cout << "Unit: " << std::endl 115 | // << "\tname: " << name.toStdString() << std::endl 116 | // << "\tdec: " << desc.toStdString() << std::endl 117 | // << "\tloaded: " << loaded.toStdString() << std::endl 118 | // << "\tactive: " << active.toStdString() << std::endl 119 | // << "\tsubstate: " << substate.toStdString() << std::endl 120 | // << "\tfollowing: " << following.toStdString() << std::endl 121 | // << "\tpath: " << path.toStdString() << std::endl 122 | // << "\tjobqueued: " << jobqueued << std::endl 123 | // << "\tjobtype: " << jobtype.toStdString() << std::endl 124 | // << "\tjobobject: " << jobobject.toStdString() << std::endl 125 | // ; 126 | 127 | this->_setRunning(active == QStringLiteral("active") && substate == QStringLiteral("running")); 128 | 129 | busArgument.endStructure(); 130 | service_found = true; 131 | break; 132 | } 133 | busArgument.endArray(); 134 | } 135 | 136 | if (!service_found) { 137 | qWarning() << "Service"<< serviceName <<"not found!"; 138 | _setRunning(false); 139 | } 140 | 141 | return isRunning; 142 | } 143 | 144 | #include 145 | 146 | void delay(uint32_t msecs, bool processQtEvents = false) 147 | { 148 | if (processQtEvents) { 149 | QTime dieTime= QTime::currentTime().addMSecs(msecs); 150 | while (QTime::currentTime() < dieTime) 151 | QCoreApplication::processEvents(QEventLoop::AllEvents, 100); 152 | } else { 153 | std::this_thread::sleep_for(std::chrono::duration(msecs)); 154 | } 155 | } 156 | 157 | #define SYSTEMD_TIMEOUT 30 158 | 159 | void SystemdService::start() 160 | { 161 | checkIsRunning(); 162 | qWarning() << "STARTING SERVICE: " << serviceName; 163 | if (isRunning) { 164 | qWarning() << "WARNING! Service " << serviceName << " was already running."; 165 | return; 166 | } 167 | systemd->call(QStringLiteral("StartUnit"), serviceName, QStringLiteral("fail")); 168 | //TODO check return of above call 169 | 170 | for(int c=0; this->isRunning; c++) { 171 | if (c > SYSTEMD_TIMEOUT*2) { // 30 seconds 172 | qWarning() << "Failed to start service " << serviceName << "after " << SYSTEMD_TIMEOUT << "seconds."; 173 | return; 174 | } 175 | qWarning() << "Starting " << serviceName << " ..."; 176 | delay(500); 177 | checkIsRunning(); 178 | } 179 | qWarning() << "Service " << serviceName << " successfully started"; 180 | } 181 | 182 | void SystemdService::stop() 183 | { 184 | checkIsRunning(); 185 | qWarning() << "STOPPING SERVICE: " << serviceName; 186 | if (!isRunning) { 187 | qWarning() << "WARNING! Service " << serviceName << " was already stopped."; 188 | return; 189 | } 190 | systemd->call(QStringLiteral("StopUnit"), serviceName, QStringLiteral("fail")); 191 | //TODO check return of above call 192 | 193 | for(int c=0; !this->isRunning; c++) { 194 | if (c > SYSTEMD_TIMEOUT*2) { // 30 seconds 195 | qWarning() << "Failed to stop service " << serviceName << "after " << SYSTEMD_TIMEOUT << "seconds."; 196 | return; 197 | } 198 | qWarning() << "Stopping " << serviceName << " ..."; 199 | delay(500); 200 | checkIsRunning(); 201 | } 202 | qWarning() << "Service " << serviceName << " successfully stopped"; 203 | } 204 | 205 | void SystemdService::restart() 206 | { 207 | checkIsRunning(); 208 | qWarning() << "RESTARTING SERVICE: " << serviceName; 209 | systemd->call(QStringLiteral("RestartUnit"), serviceName, QStringLiteral("fail")); 210 | //TODO check return of above call 211 | 212 | _setRunning(false); 213 | for(int c=0; !this->isRunning; c++) { 214 | if (c > SYSTEMD_TIMEOUT*2) { // 30 seconds 215 | qWarning() << "Failed to restart service " << serviceName << "after " << SYSTEMD_TIMEOUT << "seconds."; 216 | return; 217 | } 218 | qWarning() << "Restarting " << serviceName << " ..."; 219 | delay(500); 220 | checkIsRunning(); 221 | } 222 | qWarning() << "Service " << serviceName << " successfully restarted"; 223 | } 224 | 225 | static bool variantToString(const QVariant &arg, QString &out) 226 | { 227 | int argType = arg.userType(); 228 | if (argType == QVariant::StringList) { 229 | out += QLatin1Char('{'); 230 | const QStringList list = arg.toStringList(); 231 | for (const QString &item : list) 232 | out += QLatin1Char('\"') + item + QLatin1String("\", "); 233 | if (!list.isEmpty()) 234 | out.chop(2); 235 | out += QLatin1Char('}'); 236 | } else if (argType == QVariant::ByteArray) { 237 | out += QLatin1Char('{'); 238 | QByteArray list = arg.toByteArray(); 239 | for (int i = 0; i < list.size(); ++i) { 240 | out += QString::number(list.at(i)); 241 | out += QLatin1String(", "); 242 | } 243 | if (!list.isEmpty()) 244 | out.chop(2); 245 | out += QLatin1Char('}'); 246 | } else if (argType == QVariant::List) { 247 | out += QLatin1Char('{'); 248 | const QList list = arg.toList(); 249 | for (const QVariant &item : list) { 250 | if (!variantToString(item, out)) 251 | return false; 252 | out += QLatin1String(", "); 253 | } 254 | if (!list.isEmpty()) 255 | out.chop(2); 256 | out += QLatin1Char('}'); 257 | } else if (argType == QMetaType::Char || argType == QMetaType::Short || argType == QMetaType::Int 258 | || argType == QMetaType::Long || argType == QMetaType::LongLong) { 259 | out += QString::number(arg.toLongLong()); 260 | } else if (argType == QMetaType::UChar || argType == QMetaType::UShort || argType == QMetaType::UInt 261 | || argType == QMetaType::ULong || argType == QMetaType::ULongLong) { 262 | out += QString::number(arg.toULongLong()); 263 | } else if (argType == QMetaType::Double) { 264 | out += QString::number(arg.toDouble()); 265 | } else if (argType == QMetaType::Bool) { 266 | out += QLatin1String(arg.toBool() ? "true" : "false"); 267 | } else if (argType == qMetaTypeId()) { 268 | argToString(qvariant_cast(arg), out); 269 | } else if (argType == qMetaTypeId()) { 270 | const QString path = qvariant_cast(arg).path(); 271 | out += QLatin1String("[ObjectPath: "); 272 | out += path; 273 | out += QLatin1Char(']'); 274 | } else if (argType == qMetaTypeId()) { 275 | out += QLatin1String("[Signature: ") + qvariant_cast(arg).signature(); 276 | out += QLatin1Char(']'); 277 | } else if (argType == qMetaTypeId()) { 278 | out += QLatin1String("[Unix FD: "); 279 | out += QLatin1String(qvariant_cast(arg).isValid() ? "valid" : "not valid"); 280 | out += QLatin1Char(']'); 281 | } else if (argType == qMetaTypeId()) { 282 | const QVariant v = qvariant_cast(arg).variant(); 283 | out += QLatin1String("[Variant"); 284 | int vUserType = v.userType(); 285 | if (vUserType != qMetaTypeId() 286 | && vUserType != qMetaTypeId() 287 | && vUserType != qMetaTypeId() 288 | && vUserType != qMetaTypeId()) 289 | out += QLatin1Char('(') + QLatin1String(v.typeName()) + QLatin1Char(')'); 290 | out += QLatin1String(": "); 291 | if (!variantToString(v, out)) 292 | return false; 293 | out += QLatin1Char(']'); 294 | } else if (arg.canConvert(QVariant::String)) { 295 | out += QLatin1Char('\"') + arg.toString() + QLatin1Char('\"'); 296 | } else { 297 | out += QLatin1Char('['); 298 | out += QLatin1String(arg.typeName()); 299 | out += QLatin1Char(']'); 300 | } 301 | return true; 302 | } 303 | 304 | bool argToString(const QDBusArgument &busArg, QString &out) 305 | { 306 | QString busSig = busArg.currentSignature(); 307 | bool doIterate = false; 308 | QDBusArgument::ElementType elementType = busArg.currentType(); 309 | if (elementType != QDBusArgument::BasicType && elementType != QDBusArgument::VariantType 310 | && elementType != QDBusArgument::MapEntryType) 311 | out += QLatin1String("[Argument: ") + busSig + QLatin1Char(' '); 312 | switch (elementType) { 313 | case QDBusArgument::BasicType: 314 | case QDBusArgument::VariantType: 315 | if (!variantToString(busArg.asVariant(), out)) 316 | return false; 317 | break; 318 | case QDBusArgument::StructureType: 319 | busArg.beginStructure(); 320 | out += QLatin1Char('%'); 321 | doIterate = true; 322 | break; 323 | case QDBusArgument::ArrayType: 324 | busArg.beginArray(); 325 | out += QLatin1Char('{'); 326 | doIterate = true; 327 | break; 328 | case QDBusArgument::MapType: 329 | busArg.beginMap(); 330 | out += QLatin1Char('{'); 331 | doIterate = true; 332 | break; 333 | case QDBusArgument::MapEntryType: 334 | busArg.beginMapEntry(); 335 | if (!variantToString(busArg.asVariant(), out)) 336 | return false; 337 | out += QLatin1String(" = "); 338 | if (!argToString(busArg, out)) 339 | return false; 340 | busArg.endMapEntry(); 341 | break; 342 | case QDBusArgument::UnknownType: 343 | default: 344 | out += QLatin1String(""); 345 | return false; 346 | } 347 | if (doIterate && !busArg.atEnd()) { 348 | while (!busArg.atEnd()) { 349 | if (!argToString(busArg, out)) 350 | return false; 351 | out += QLatin1String(", "); 352 | } 353 | out.chop(2); 354 | } 355 | switch (elementType) { 356 | case QDBusArgument::BasicType: 357 | case QDBusArgument::VariantType: 358 | case QDBusArgument::UnknownType: 359 | case QDBusArgument::MapEntryType: 360 | // nothing to do 361 | break; 362 | case QDBusArgument::StructureType: 363 | busArg.endStructure(); 364 | out += QLatin1Char('%'); 365 | break; 366 | case QDBusArgument::ArrayType: 367 | out += QLatin1Char('}'); 368 | busArg.endArray(); 369 | break; 370 | case QDBusArgument::MapType: 371 | out += QLatin1Char('}'); 372 | busArg.endMap(); 373 | break; 374 | } 375 | if (elementType != QDBusArgument::BasicType && elementType != QDBusArgument::VariantType 376 | && elementType != QDBusArgument::MapEntryType) 377 | out += QLatin1Char(']'); 378 | return true; 379 | } 380 | -------------------------------------------------------------------------------- /src/systemdservice.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the pipecontrol project. 3 | * Copyright (c) 2022 Matteo De Carlo. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, but 10 | * WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #pragma once 18 | #include 19 | #include 20 | 21 | class SystemdService : public QObject 22 | { 23 | Q_OBJECT 24 | Q_PROPERTY(bool running READ running WRITE setRunning NOTIFY runningChanged) 25 | Q_SIGNALS: 26 | void runningChanged(bool); 27 | 28 | private: 29 | QString serviceName; 30 | QDBusConnection bus; 31 | QDBusInterface *systemd = nullptr; 32 | bool isRunning = false; 33 | 34 | public: 35 | SystemdService(QString serviceName, bool userService = true); 36 | ~SystemdService(); 37 | 38 | bool running() { return isRunning; } 39 | void setRunning(bool); 40 | 41 | bool checkIsRunning(); 42 | Q_INVOKABLE void start(); 43 | Q_INVOKABLE void stop(); 44 | Q_INVOKABLE void restart(); 45 | 46 | private: 47 | // Sets variable, emits signal but does not communicate with systemd 48 | void _setRunning(bool); 49 | }; 50 | --------------------------------------------------------------------------------