├── .vscode ├── c_cpp_properties.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── Makefile ├── README.md ├── meson.build ├── pw.c └── pw_time_print.c /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [], 9 | "compilerPath": "/usr/bin/clang", 10 | "cStandard": "c17", 11 | "cppStandard": "c++14", 12 | "intelliSenseMode": "clang-x64", 13 | "compileCommands": "${workspaceFolder}/builddir/compile_commands.json" 14 | } 15 | ], 16 | "version": 4 17 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "(gdb) Launch", 9 | "type": "cppdbg", 10 | "request": "launch", 11 | "program": "/usr/local/bin/deadbeef", 12 | "args": [], 13 | "stopAtEntry": false, 14 | "cwd": "${workspaceFolder}", 15 | "environment": [], 16 | "externalConsole": false, 17 | "MIMode": "gdb", 18 | "preLaunchTask": "Deploy", 19 | "setupCommands": [ 20 | { 21 | "description": "Enable pretty-printing for gdb", 22 | "text": "-enable-pretty-printing", 23 | "ignoreFailures": true 24 | } 25 | ] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "clangd.arguments": [ 3 | "-background-index", 4 | "-compile-commands-dir=builddir" 5 | ], 6 | "files.associations": { 7 | "array": "c", 8 | "string_view": "c", 9 | "initializer_list": "c", 10 | "ringbuffer.h": "c", 11 | "limits.h": "c" 12 | }, 13 | "C_Cpp.default.compileCommands": "builddir/compile_commands.json", 14 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "shell", 6 | "label": "Meson: Build Project", 7 | "options": { 8 | "cwd": "${workspaceFolder}/builddir" 9 | }, 10 | "command": "meson compile", 11 | "problemMatcher": [ 12 | { 13 | "base": "$gcc", 14 | "fileLocation": ["relative", "${workspaceFolder}/builddir"] 15 | } 16 | ] 17 | }, 18 | { 19 | "label": "Deploy", 20 | "type": "shell", 21 | "command": "cp ${workspaceFolder}/builddir/ddb_out_pw.so ~/.local/lib64/deadbeef/", 22 | "problemMatcher": [], 23 | "dependsOn": ["Meson: Build Project"], 24 | "group": { 25 | "kind": "build", 26 | "isDefault": true 27 | } 28 | }, 29 | { 30 | "type": "shell", 31 | "label": "Clang-Tidy", 32 | "command": "clang-tidy -p builddir -checks=\"*\" pw.c", 33 | "problemMatcher": [ 34 | { 35 | "base": "$gcc", 36 | } 37 | ] 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC?=clang 2 | CFLAGS?=-I/usr/local/include -DPW_ENABLE_DEPRECATED 3 | 4 | all: 5 | $(CC) $(CFLAGS) -std=c99 -shared -O2 -o ddb_out_pw.so pw.c `pkg-config --cflags --libs libpipewire-0.3` -fPIC -Wall -march=native 6 | debug: CFLAGS += -DDDBPW_DEBUG -g 7 | debug: all 8 | 9 | install: all 10 | cp ddb_out_pw.so ~/.local/lib64/deadbeef/ 11 | installdebug: debug install 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PipeWire output plugin for DeaDBeeF 2 | 3 | This is a draft for a PipeWire plugin for DeaDBeeF Music Player 4 | 5 | Build using meson: 6 | 7 | $ meson setup builddir 8 | $ meson compile -C builddir 9 | 10 | Then install: 11 | 12 | $ cp builddir/ddb_out_pw.so ~/.local/lib64/deadbeef 13 | 14 | Remember to have the Deadbeef development package installed or `deadbeef.h` available in standard search paths (`/usr/include/deadbeef` or `/usr/local/include/deadbeef`). 15 | 16 | You can instruct meson to search for include files like so: 17 | 18 | $ C_INCLUDE_PATH=/opt meson setup 19 | 20 | This assumes the header file is in `/opt/deadbeef` directory. 21 | 22 | 23 | New plugin settings UI: 24 | 25 | ![Screenshot](../assets/plugin-settings-newui.png?raw=true) 26 | 27 | 28 | Older plugin settings UI: 29 | 30 | ![Screenshot](../assets/pipewire-options.png?raw=true) 31 | 32 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('ddb_output_pw', 'c', 2 | version: '0.1', 3 | default_options: [ 4 | 'buildtype=debugoptimized', 5 | 'warning_level=1', 6 | ], 7 | license: 'GPLv2.0+') 8 | 9 | 10 | debug_cflags = [] 11 | buildtype = get_option('buildtype') 12 | if buildtype.startswith('debug') 13 | debug_cflags += '-DDDBPW_DEBUG' 14 | endif 15 | 16 | add_project_arguments(debug_cflags, language: 'c') 17 | 18 | add_project_arguments('-DPW_ENABLE_DEPRECATED', language: 'c') 19 | 20 | 21 | cc = meson.get_compiler('c') 22 | if not cc.has_header('deadbeef/deadbeef.h') 23 | error('missing header: deadbeef.h') 24 | endif 25 | 26 | pw_dep = dependency('libpipewire-0.3') 27 | 28 | shared_library('ddb_out_pw', 'pw.c', dependencies : [pw_dep], name_prefix: '', 29 | install: true, install_dir: 'lib/deadbeef') 30 | -------------------------------------------------------------------------------- /pw.c: -------------------------------------------------------------------------------- 1 | /* 2 | PipeWire output plugin for DeaDBeeF Player 3 | Copyright (C) 2020 Nicolai Syvertsen 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, either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | #ifdef DDB_IN_TREE 27 | #include "../../deadbeef.h" 28 | #else 29 | #include 30 | #endif 31 | 32 | #define OP_ERROR_SUCCESS 0 33 | #define OP_ERROR_INTERNAL -1 34 | 35 | #define CONFSTR_DDBPW_VOLUMECONTROL "pipewire.volumecontrol" 36 | #define DDBPW_DEFAULT_VOLUMECONTROL 0 37 | #define CONFSTR_DDBPW_REMOTENAME "pipewire.remotename" 38 | #define CONFSTR_DDBPW_PROPS "pipewire.properties" 39 | #define DDBPW_DEFAULT_REMOTENAME "" 40 | 41 | 42 | #ifdef ENABLE_BUFFER_OPTION 43 | #define CONFSTR_DDBPW_BUFLENGTH "pipewire.buflength" 44 | #endif 45 | #define DDBPW_DEFAULT_BUFLENGTH 25 46 | 47 | #ifdef DDBPW_DEBUG 48 | #define trace(...) { fprintf(stdout, __VA_ARGS__); } 49 | #else 50 | #define trace(fmt,...) 51 | #endif 52 | 53 | #define log_err(...) { deadbeef->log_detailed (&plugin.plugin, DDB_LOG_LAYER_DEFAULT, __VA_ARGS__); } 54 | 55 | DB_functions_t * deadbeef; 56 | static DB_output_t plugin; 57 | static char plugin_description[1024]; 58 | 59 | #define PW_PLUGIN_ID "pipewire" 60 | 61 | static const char application_title[] = "DeaDBeeF Music Player"; 62 | static const char application_id[] = "music.deadbeef.player"; 63 | 64 | static char *tfbytecode; 65 | 66 | static ddb_waveformat_t requested_fmt; 67 | static ddb_playback_state_t state=DDB_PLAYBACK_STATE_STOPPED; 68 | static uintptr_t mutex; 69 | static int _setformat_requested; 70 | static float _initialvol; 71 | static int _buffersize; 72 | static int _stride; 73 | 74 | struct data { 75 | struct pw_thread_loop *loop; 76 | struct pw_stream *stream; 77 | int pw_has_init; 78 | }; 79 | 80 | struct data data = { 0, }; 81 | 82 | static int ddbpw_init(void); 83 | 84 | static int ddbpw_free(void); 85 | 86 | static int ddbpw_setformat(ddb_waveformat_t *fmt); 87 | 88 | static int ddbpw_play(void); 89 | 90 | static int ddbpw_stop(void); 91 | 92 | static int ddbpw_pause(void); 93 | 94 | static int ddbpw_unpause(void); 95 | 96 | static int ddbpw_set_spec(ddb_waveformat_t *fmt); 97 | 98 | static void my_pw_init(void) { 99 | if (data.pw_has_init || state != DDB_PLAYBACK_STATE_STOPPED) { 100 | return; 101 | } 102 | pw_init(NULL, NULL); 103 | data.pw_has_init = 1; 104 | } 105 | 106 | static void my_pw_deinit(void) { 107 | if (!data.pw_has_init || state != DDB_PLAYBACK_STATE_STOPPED) { 108 | return; 109 | } 110 | pw_deinit(); 111 | data.pw_has_init = 0; 112 | } 113 | 114 | static int _apply_format(struct spa_loop *loop, 115 | bool async, 116 | uint32_t seq, 117 | const void *_data, 118 | size_t size, 119 | void *user_data) { 120 | deadbeef->mutex_lock(mutex); 121 | 122 | pw_stream_disconnect(data.stream); 123 | ddbpw_set_spec(&requested_fmt); 124 | _setformat_requested = 0; 125 | 126 | deadbeef->mutex_unlock(mutex); 127 | 128 | trace("From inside loop invoke function! %d\n", seq); 129 | return 0; 130 | } 131 | 132 | static void on_process(void *userdata) { 133 | #ifdef DDBPW_DEBUG 134 | static int counter; 135 | #endif 136 | struct data *data = userdata; 137 | struct pw_buffer *b = NULL; 138 | struct spa_buffer *buf = NULL; 139 | int16_t *dst = NULL; 140 | 141 | if (!_setformat_requested) { 142 | 143 | if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) { 144 | pw_log_warn("out of buffers: %m"); 145 | return; 146 | } 147 | 148 | buf = b->buffer; 149 | if ((dst = buf->datas[0].data) == NULL) { 150 | return; 151 | } 152 | 153 | #ifdef ENABLE_BUFFER_OPTION 154 | uint32_t buffersize = _buffersize; 155 | uint32_t nframes = SPA_MIN(buffersize, buf->datas[0].maxsize/_stride); 156 | #else 157 | uint32_t buffersize = _buffersize; 158 | uint32_t nframes = SPA_MIN(buffersize, buf->datas[0].maxsize/_stride); 159 | #endif 160 | 161 | #if PW_CHECK_VERSION(0, 3, 49) 162 | if (b->requested != 0) { 163 | nframes = SPA_MIN(b->requested, nframes); 164 | } 165 | #endif 166 | 167 | int len = nframes * _stride; 168 | int bytesread=0; 169 | if (deadbeef->streamer_ok_to_read(-1)) { 170 | bytesread = deadbeef->streamer_read (buf->datas[0].data , len); 171 | } 172 | // if (bytesread != 0) { 173 | // b->size = bytesread / _stride; 174 | // } 175 | if (bytesread < len) { 176 | spa_memzero(buf->datas[0].data+bytesread, len-bytesread); 177 | } 178 | 179 | buf->datas[0].chunk->offset = 0; 180 | buf->datas[0].chunk->stride = _stride; 181 | buf->datas[0].chunk->size = bytesread; 182 | 183 | trace("%d len: %d stride: %d requested: %ld nframes: %d maxsize: %u (/ stride %d) _buffersize %d bytesread %d\n", 184 | counter++, len, _stride, b->requested, nframes, buf->datas[0].maxsize, buf->datas[0].maxsize / _stride, buffersize, bytesread); 185 | 186 | pw_stream_queue_buffer(data->stream, b); 187 | } 188 | } 189 | 190 | static void 191 | set_volume(int dolock, float volume) { 192 | if (data.stream && state != DDB_PLAYBACK_STATE_STOPPED) { 193 | float vol[SPA_AUDIO_MAX_CHANNELS] = {0}; 194 | 195 | for (int i = 0; i < plugin.fmt.channels; i++) { 196 | vol[i] = volume; 197 | } 198 | 199 | if (dolock) { 200 | pw_thread_loop_lock(data.loop); 201 | } 202 | pw_stream_set_control(data.stream, SPA_PROP_channelVolumes, plugin.fmt.channels, vol, 0); 203 | if (dolock) { 204 | pw_thread_loop_unlock(data.loop); 205 | } 206 | } 207 | } 208 | 209 | static void on_state_changed(void *_data, enum pw_stream_state old, 210 | enum pw_stream_state pwstate, const char *error) { 211 | trace("PipeWire: Stream state %s\n", pw_stream_state_as_string(pwstate)); 212 | 213 | if (_setformat_requested) { 214 | return; 215 | } 216 | 217 | if (pwstate == PW_STREAM_STATE_ERROR || (state == DDB_PLAYBACK_STATE_PLAYING && pwstate == PW_STREAM_STATE_UNCONNECTED ) ) { 218 | log_err("PipeWire: Stream error: %s\n", error); 219 | deadbeef->sendmessage(DB_EV_STOP, 0, 0, 0); 220 | } 221 | } 222 | 223 | static void on_control_info(void *_data, uint32_t id, const struct pw_stream_control *control) { 224 | #ifdef DDBPW_DEBUG 225 | 226 | fprintf(stderr, "PipeWire: Control %s", control->name); 227 | for (int i = 0; i < control->n_values; i++) { 228 | fprintf(stderr, " value[%d] = %f", i, control->values[i]); 229 | } 230 | fprintf(stderr, "\n"); 231 | #endif 232 | 233 | if (id == SPA_PROP_channelVolumes && plugin.has_volume) { 234 | float dbvol = deadbeef->volume_get_amp(); 235 | int changedvolume = 0; 236 | for (int i = 0; i < control->n_values; i++) { 237 | if (control->values[i] != dbvol) { 238 | changedvolume = i; 239 | break; 240 | } 241 | } 242 | 243 | deadbeef->volume_set_amp(control->values[changedvolume]); 244 | } 245 | } 246 | 247 | static void on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param) { 248 | if (id != SPA_PARAM_Format || param == NULL) { 249 | return; 250 | } 251 | 252 | if (plugin.has_volume) { 253 | set_volume(0, _initialvol); 254 | } 255 | 256 | #ifdef ENABLE_BUFFER_OPTION 257 | { 258 | const struct spa_pod *params[1]; 259 | uint8_t buffer[4096]; 260 | struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); 261 | int stride = plugin.fmt.channels * (plugin.fmt.bps/8); 262 | int size = _buffersize*stride; 263 | 264 | params[0] = spa_pod_builder_add_object(&b, 265 | SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, 266 | SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), 267 | SPA_PARAM_BUFFERS_size, SPA_POD_Int(size), 268 | SPA_PARAM_BUFFERS_stride, SPA_POD_Int(stride)); 269 | 270 | pw_stream_update_params(data.stream, params, 1); 271 | } 272 | #endif 273 | 274 | } 275 | 276 | static const struct pw_stream_events stream_events = { 277 | PW_VERSION_STREAM_EVENTS, 278 | .process = on_process, 279 | .state_changed = on_state_changed, 280 | .control_info = on_control_info, 281 | .param_changed = on_param_changed, 282 | }; 283 | 284 | static void do_update_media_props(DB_playItem_t *track, struct pw_properties *props) { 285 | int rc = 0, notrackgiven=0; 286 | 287 | ddb_tf_context_t ctx = { 288 | ._size = sizeof(ddb_tf_context_t), 289 | .flags = DDB_TF_CONTEXT_NO_DYNAMIC, 290 | .plt = NULL, 291 | .iter = PL_MAIN 292 | }; 293 | 294 | if (!track) { 295 | track = deadbeef->streamer_get_playing_track_safe(); 296 | if (track == NULL) { 297 | return; 298 | } 299 | notrackgiven = 1; 300 | } 301 | 302 | struct spa_dict_item items[3] = {0}; 303 | int n_items = 0; 304 | 305 | char buf[1000] = {0}; 306 | const char *artist = NULL; 307 | const char *title = NULL; 308 | 309 | ctx.it = track; 310 | if (deadbeef->tf_eval(&ctx, tfbytecode, buf, sizeof(buf)) > 0) { 311 | items[n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_NAME, buf); 312 | } 313 | 314 | deadbeef->pl_lock(); 315 | artist = deadbeef->pl_find_meta(track, "artist"); 316 | title = deadbeef->pl_find_meta(track, "title"); 317 | 318 | if (artist) { 319 | items[n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_ARTIST, artist); 320 | } 321 | 322 | if (title) { 323 | items[n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_TITLE, title); 324 | } 325 | 326 | if (props) { 327 | pw_properties_update(props, &SPA_DICT_INIT(items, n_items)); 328 | } else { 329 | rc = pw_stream_update_properties(data.stream, &SPA_DICT_INIT(items, n_items)); 330 | if (rc < 0) { 331 | trace("PipeWire: Error updating properties!\n"); 332 | } 333 | } 334 | 335 | deadbeef->pl_unlock(); 336 | if (notrackgiven) { 337 | deadbeef->pl_item_unref(track); 338 | } 339 | } 340 | 341 | static int ddbpw_init(void) { 342 | trace ("ddbpw_init\n"); 343 | 344 | my_pw_init(); 345 | 346 | state = DDB_PLAYBACK_STATE_STOPPED; 347 | _setformat_requested = 0; 348 | _buffersize = 0; 349 | 350 | if (requested_fmt.samplerate != 0) { 351 | memcpy (&plugin.fmt, &requested_fmt, sizeof (ddb_waveformat_t)); 352 | } 353 | 354 | data.loop = pw_thread_loop_new("ddb_out_pw", NULL); 355 | 356 | char dev[256] = {0}; 357 | char remote[256] = {0}; 358 | char propstr[256] = {0}; 359 | deadbeef->conf_get_str (PW_PLUGIN_ID "_soundcard", "default", dev, sizeof(dev)); 360 | 361 | deadbeef->conf_get_str(CONFSTR_DDBPW_REMOTENAME, DDBPW_DEFAULT_REMOTENAME, remote, sizeof(remote)); 362 | 363 | deadbeef->conf_get_str(CONFSTR_DDBPW_PROPS, "", propstr, sizeof(propstr)); 364 | 365 | struct pw_properties *props = pw_properties_new( 366 | PW_KEY_REMOTE_NAME, (remote[0] ? remote: NULL), 367 | PW_KEY_NODE_NAME, application_title, 368 | PW_KEY_APP_NAME, application_title, 369 | PW_KEY_APP_ID, application_id, 370 | PW_KEY_APP_ICON_NAME, "deadbeef", 371 | PW_KEY_MEDIA_TYPE, "Audio", 372 | PW_KEY_MEDIA_CATEGORY, "Playback", 373 | PW_KEY_MEDIA_ROLE, "Music", 374 | PW_KEY_NODE_TARGET, (!strcmp(dev, "default")) ? NULL: dev, 375 | NULL); 376 | do_update_media_props(NULL, props); 377 | pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", plugin.fmt.samplerate); 378 | 379 | pw_properties_update_string(props, propstr, strlen(propstr)); 380 | 381 | data.stream = pw_stream_new_simple( 382 | pw_thread_loop_get_loop(data.loop), 383 | application_title, 384 | props, 385 | &stream_events, 386 | &data); 387 | 388 | if (!data.stream) { 389 | log_err("PipeWire: Error creating stream!"); 390 | return OP_ERROR_INTERNAL; 391 | } 392 | 393 | 394 | return OP_ERROR_SUCCESS; 395 | } 396 | 397 | static int ddbpw_setformat (ddb_waveformat_t *fmt) { 398 | trace("Pipewire: setformat called!\n"); 399 | deadbeef->mutex_lock(mutex); 400 | _setformat_requested = 1; 401 | memcpy (&requested_fmt, fmt, sizeof (ddb_waveformat_t)); 402 | 403 | if (data.stream == 0) { 404 | deadbeef->mutex_unlock(mutex); 405 | return 0; 406 | } 407 | pw_thread_loop_lock(data.loop); 408 | pw_stream_set_active(data.stream, false); 409 | pw_loop_invoke(pw_thread_loop_get_loop(data.loop), _apply_format, 1, NULL, 0, false, NULL); 410 | pw_thread_loop_unlock(data.loop); 411 | 412 | deadbeef->mutex_unlock(mutex); 413 | return 0; 414 | } 415 | 416 | static int ddbpw_free(void) { 417 | trace("ddbpw_free\n"); 418 | 419 | state = DDB_PLAYBACK_STATE_STOPPED; 420 | 421 | if (!data.loop) { 422 | return 0; 423 | } 424 | deadbeef->mutex_lock(mutex); 425 | 426 | pw_thread_loop_stop(data.loop); 427 | 428 | pw_stream_destroy(data.stream); 429 | data.stream = NULL; 430 | 431 | pw_thread_loop_destroy(data.loop); 432 | data.loop = NULL; 433 | deadbeef->mutex_unlock(mutex); 434 | my_pw_deinit(); 435 | return OP_ERROR_SUCCESS; 436 | } 437 | 438 | static void set_channel_map(int channels, struct spa_audio_info_raw* audio_info) { 439 | /* Following http://www.microsoft.com/whdc/device/audio/multichaud.mspx#EKLAC */ 440 | 441 | switch (channels) { 442 | case 1: 443 | audio_info->position[0] = SPA_AUDIO_CHANNEL_MONO; 444 | return; 445 | 446 | case 18: 447 | audio_info->position[15] = SPA_AUDIO_CHANNEL_TRL; 448 | audio_info->position[16] = SPA_AUDIO_CHANNEL_TRC; 449 | audio_info->position[17] = SPA_AUDIO_CHANNEL_TRR; 450 | /* Fall through */ 451 | 452 | case 15: 453 | audio_info->position[12] = SPA_AUDIO_CHANNEL_TFL; 454 | audio_info->position[13] = SPA_AUDIO_CHANNEL_TFC; 455 | audio_info->position[14] = SPA_AUDIO_CHANNEL_TFR; 456 | /* Fall through */ 457 | 458 | case 12: 459 | audio_info->position[11] = SPA_AUDIO_CHANNEL_TC; 460 | /* Fall through */ 461 | 462 | case 11: 463 | audio_info->position[9] = SPA_AUDIO_CHANNEL_SL; 464 | audio_info->position[10] = SPA_AUDIO_CHANNEL_SR; 465 | /* Fall through */ 466 | 467 | case 9: 468 | audio_info->position[8] = SPA_AUDIO_CHANNEL_RC; 469 | /* Fall through */ 470 | 471 | case 8: 472 | audio_info->position[6] = SPA_AUDIO_CHANNEL_FLC; 473 | audio_info->position[7] = SPA_AUDIO_CHANNEL_FRC; 474 | /* Fall through */ 475 | 476 | case 6: 477 | audio_info->position[4] = SPA_AUDIO_CHANNEL_RL; 478 | audio_info->position[5] = SPA_AUDIO_CHANNEL_RR; 479 | /* Fall through */ 480 | 481 | case 4: 482 | audio_info->position[3] = SPA_AUDIO_CHANNEL_LFE; 483 | /* Fall through */ 484 | 485 | case 3: 486 | audio_info->position[2] = SPA_AUDIO_CHANNEL_FC; 487 | /* Fall through */ 488 | 489 | case 2: 490 | audio_info->position[0] = SPA_AUDIO_CHANNEL_FL; 491 | audio_info->position[1] = SPA_AUDIO_CHANNEL_FR; 492 | 493 | } 494 | } 495 | 496 | static struct spa_pod * makeformat(ddb_waveformat_t *fmt, uint8_t *buffer, size_t buffer_size) { 497 | 498 | enum spa_audio_format pwfmt = 0; 499 | 500 | switch (fmt->bps) { 501 | case 8: 502 | pwfmt = SPA_AUDIO_FORMAT_S8; 503 | break; 504 | case 16: 505 | pwfmt = SPA_AUDIO_FORMAT_S16_LE; 506 | break; 507 | case 24: 508 | pwfmt = SPA_AUDIO_FORMAT_S24_LE; 509 | break; 510 | case 32: 511 | if (fmt->is_float) { 512 | pwfmt = SPA_AUDIO_FORMAT_F32_LE; 513 | } 514 | else { 515 | pwfmt = SPA_AUDIO_FORMAT_S32_LE; 516 | } 517 | break; 518 | default: 519 | return NULL; 520 | }; 521 | 522 | 523 | 524 | struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, buffer_size); 525 | 526 | struct spa_audio_info_raw rawinfo = SPA_AUDIO_INFO_RAW_INIT( 527 | .flags = 0, 528 | .format = pwfmt, 529 | .channels = fmt->channels, 530 | .rate = fmt->samplerate 531 | ); 532 | 533 | set_channel_map(fmt->channels, &rawinfo); 534 | 535 | return spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &rawinfo); 536 | 537 | } 538 | 539 | static int ddbpw_set_spec(ddb_waveformat_t *fmt) { 540 | memcpy (&plugin.fmt, fmt, sizeof (ddb_waveformat_t)); 541 | if (!plugin.fmt.channels) { 542 | // generic format 543 | plugin.fmt.bps = 16; 544 | plugin.fmt.is_float = 0; 545 | plugin.fmt.channels = 2; 546 | plugin.fmt.samplerate = 44100; 547 | plugin.fmt.channelmask = 3; 548 | } 549 | 550 | trace ("format %dbit %s %dch %dHz channelmask=%X\n", plugin.fmt.bps, plugin.fmt.is_float ? "float" : "int", plugin.fmt.channels, plugin.fmt.samplerate, plugin.fmt.channelmask); 551 | _stride = plugin.fmt.channels * (plugin.fmt.bps / 8); 552 | 553 | uint8_t spa_buffer[1024]; 554 | const struct spa_pod *params[1] = { 555 | makeformat(&plugin.fmt, spa_buffer, sizeof (spa_buffer)) 556 | }; 557 | 558 | struct pw_properties *props = pw_properties_new(NULL, NULL); 559 | #ifdef ENABLE_BUFFER_OPTION 560 | _buffersize = deadbeef->conf_get_int(CONFSTR_DDBPW_BUFLENGTH, DDBPW_DEFAULT_BUFLENGTH) * plugin.fmt.samplerate / 1000; 561 | pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%d/%u", _buffersize, plugin.fmt.samplerate); 562 | #else 563 | _buffersize = DDBPW_DEFAULT_BUFLENGTH * plugin.fmt.samplerate / 1000; 564 | pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%d/%u", _buffersize, plugin.fmt.samplerate); 565 | #endif 566 | 567 | pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", plugin.fmt.samplerate); 568 | pw_stream_update_properties(data.stream, &props->dict); 569 | pw_properties_free(props); 570 | 571 | if (0 != pw_stream_connect(data.stream, 572 | PW_DIRECTION_OUTPUT, 573 | PW_ID_ANY, 574 | PW_STREAM_FLAG_AUTOCONNECT | 575 | PW_STREAM_FLAG_MAP_BUFFERS | 576 | PW_STREAM_FLAG_RT_PROCESS, 577 | params, 1)) { 578 | log_err("PipeWire: Error connecting stream!\n"); 579 | if (pw_properties_get(pw_stream_get_properties(data.stream), PW_KEY_REMOTE_NAME)) { 580 | log_err("PipeWire: Please check if remote daemon name is valid and daemon is up.\n") 581 | } 582 | return OP_ERROR_INTERNAL; 583 | }; 584 | 585 | 586 | state = DDB_PLAYBACK_STATE_PLAYING; 587 | 588 | return OP_ERROR_SUCCESS; 589 | } 590 | 591 | static void update_has_volume(void) { 592 | plugin.has_volume = deadbeef->conf_get_int(CONFSTR_DDBPW_VOLUMECONTROL, DDBPW_DEFAULT_VOLUMECONTROL); 593 | } 594 | 595 | static int ddbpw_play(void) { 596 | trace ("ddbpw_play\n"); 597 | 598 | deadbeef->mutex_lock(mutex); 599 | 600 | update_has_volume(); 601 | _initialvol = plugin.has_volume ? deadbeef->volume_get_amp() : 1.0f; 602 | 603 | if (!data.loop) { 604 | ddbpw_init(); 605 | } 606 | 607 | 608 | int ret = ddbpw_set_spec(&plugin.fmt); 609 | pw_thread_loop_start(data.loop); 610 | if (ret != 0) { 611 | ddbpw_free(); 612 | } 613 | deadbeef->mutex_unlock(mutex); 614 | return ret; 615 | } 616 | 617 | static int ddbpw_stop(void) { 618 | ddbpw_free(); 619 | 620 | return OP_ERROR_SUCCESS; 621 | } 622 | 623 | static int ddbpw_pause(void) { 624 | if (!data.loop && ddbpw_play() != OP_ERROR_SUCCESS) { 625 | return OP_ERROR_INTERNAL; 626 | } 627 | 628 | // set pause state 629 | state = DDB_PLAYBACK_STATE_PAUSED; 630 | pw_thread_loop_lock(data.loop); 631 | pw_stream_flush(data.stream, 0); 632 | pw_stream_set_active(data.stream, 0); 633 | pw_thread_loop_unlock(data.loop); 634 | return OP_ERROR_SUCCESS; 635 | } 636 | 637 | static int ddbpw_unpause(void) { 638 | // unset pause state 639 | if (state == DDB_PLAYBACK_STATE_PAUSED) { 640 | state = DDB_PLAYBACK_STATE_PLAYING; 641 | } 642 | pw_thread_loop_lock(data.loop); 643 | pw_stream_set_active(data.stream, 1); 644 | pw_thread_loop_unlock(data.loop); 645 | return OP_ERROR_SUCCESS; 646 | } 647 | 648 | 649 | static ddb_playback_state_t ddbpw_get_state(void) { 650 | return state; 651 | } 652 | 653 | 654 | 655 | static int ddbpw_plugin_start(void) { 656 | mutex = deadbeef->mutex_create(); 657 | 658 | tfbytecode = deadbeef->tf_compile("[%artist% - ]%title%"); 659 | return 0; 660 | } 661 | 662 | static int ddbpw_plugin_stop(void) { 663 | deadbeef->mutex_free(mutex); 664 | deadbeef->tf_free(tfbytecode); 665 | return 0; 666 | } 667 | 668 | DB_plugin_t * ddb_out_pw_load(DB_functions_t *api) { 669 | deadbeef = api; 670 | snprintf(plugin_description, sizeof(plugin_description), 671 | "This is a PipeWire plugin.\nLinked to library version %s\n", pw_get_library_version()); 672 | plugin.plugin.descr = plugin_description; 673 | return DB_PLUGIN (&plugin); 674 | } 675 | 676 | static int 677 | ddbpw_message (uint32_t id, uintptr_t ctx, uint32_t p1, uint32_t p2) { 678 | switch (id) { 679 | case DB_EV_SONGSTARTED: 680 | if (state == DDB_PLAYBACK_STATE_PLAYING) { 681 | pw_thread_loop_lock(data.loop); 682 | do_update_media_props(((ddb_event_track_t *)ctx)->track, NULL); 683 | pw_thread_loop_unlock(data.loop); 684 | } 685 | break; 686 | case DB_EV_VOLUMECHANGED: 687 | if (plugin.has_volume) { 688 | set_volume(1, deadbeef->volume_get_amp()); 689 | } 690 | break; 691 | case DB_EV_CONFIGCHANGED: 692 | update_has_volume(); 693 | if (plugin.has_volume) { 694 | set_volume(1, deadbeef->volume_get_amp()); 695 | } else { 696 | set_volume(1, 1.0f); 697 | } 698 | break; 699 | } 700 | return 0; 701 | } 702 | 703 | struct enum_card_userdata { 704 | void (*callback)(const char *name, const char *desc, void *); 705 | void *userdata; 706 | }; 707 | 708 | static void registry_event_global(void *data, uint32_t id, 709 | uint32_t permissions, const char *type, uint32_t version, 710 | const struct spa_dict *props) { 711 | struct enum_card_userdata *enumuserdata = (struct enum_card_userdata *)data; 712 | 713 | if (!strcmp(type, PW_TYPE_INTERFACE_Node) && props) { 714 | const char *media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); 715 | 716 | if (media_class && (!strcmp(media_class, "Audio/Sink") || !strcmp(media_class, "Audio/Duplex"))) { 717 | const char *desc = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION); 718 | const char *name = spa_dict_lookup(props, PW_KEY_NODE_NAME); 719 | char name_buf[64]; 720 | if (!name) { 721 | snprintf(name_buf, sizeof(name_buf), "%d", id); 722 | name = name_buf; 723 | } 724 | 725 | if (!desc) { 726 | desc = name; 727 | } 728 | 729 | // Avoid crazy long descriptions, they grow the GTK dropdown box in deadbeef GUI 730 | // Truncate with a middle ellipsis so we catch output port names that are always at the end 731 | char buf[256] = { 0 }; 732 | if (strlen(desc) > 80) { 733 | strncpy (buf, desc, 38); 734 | strcat(buf, "..."); 735 | strcat (buf, desc+strlen(desc)-38); 736 | 737 | } 738 | else { 739 | strcpy(buf, desc ? desc : ""); 740 | } 741 | 742 | enumuserdata->callback(name, buf, enumuserdata->userdata); 743 | } 744 | } 745 | } 746 | 747 | static const struct pw_registry_events registry_events = { 748 | PW_VERSION_REGISTRY_EVENTS, 749 | .global = registry_event_global, 750 | }; 751 | 752 | struct donedata { 753 | int pending; 754 | int done; 755 | struct pw_main_loop *loop; 756 | }; 757 | 758 | void core_event_done(void *object, uint32_t id, int seq) { 759 | struct donedata *donedata = (struct donedata *)object; 760 | if (id == PW_ID_CORE && seq == donedata->pending) { 761 | donedata->done = 1; 762 | pw_main_loop_quit(donedata->loop); 763 | } 764 | } 765 | 766 | static int roundtrip(struct pw_core *core, struct pw_main_loop *loop) { 767 | struct spa_hook core_listener; 768 | struct donedata donedata = {0,0, loop}; 769 | 770 | const struct pw_core_events core_events = { 771 | PW_VERSION_CORE_EVENTS, 772 | .done = core_event_done, 773 | }; 774 | 775 | spa_zero(core_listener); 776 | pw_core_add_listener(core, &core_listener, 777 | &core_events, &donedata); 778 | 779 | donedata.pending = pw_core_sync(core, PW_ID_CORE, 0); 780 | 781 | while (!donedata.done) { 782 | pw_main_loop_run(loop); 783 | } 784 | spa_hook_remove(&core_listener); 785 | return 0; 786 | } 787 | 788 | static void 789 | ddbpw_enum_soundcards(void (*callback)(const char *name, const char *desc, void *), void *userdata) { 790 | struct pw_main_loop *loop = NULL; 791 | struct pw_context *context = NULL; 792 | struct pw_core *core = NULL; 793 | struct pw_registry *registry = NULL; 794 | struct spa_hook registry_listener = {0}; 795 | struct enum_card_userdata enumuserdata = {0}; 796 | 797 | my_pw_init(); 798 | 799 | loop = pw_main_loop_new(NULL /* properties */); 800 | context = pw_context_new(pw_main_loop_get_loop(loop), 801 | NULL /* properties */, 802 | 0 /* user_data size */); 803 | if (!context) { 804 | return; 805 | } 806 | 807 | char remote[256] = { 0 }; 808 | deadbeef->conf_get_str(CONFSTR_DDBPW_REMOTENAME, DDBPW_DEFAULT_REMOTENAME, remote, sizeof(remote)); 809 | 810 | core = pw_context_connect(context, 811 | pw_properties_new( 812 | PW_KEY_REMOTE_NAME, (remote[0] ? remote: NULL), 813 | NULL), 814 | 0 /* user_data size */); 815 | if (!core) { 816 | return; 817 | } 818 | 819 | registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, 820 | 0 /* user_data size */); 821 | if (!registry) { 822 | return; 823 | } 824 | 825 | enumuserdata.callback = callback; 826 | enumuserdata.userdata = userdata; 827 | 828 | spa_zero(registry_listener); 829 | pw_registry_add_listener(registry, ®istry_listener, 830 | ®istry_events, &enumuserdata); 831 | 832 | roundtrip(core, loop); 833 | 834 | pw_proxy_destroy((struct pw_proxy *)registry); 835 | pw_core_disconnect(core); 836 | pw_context_destroy(context); 837 | pw_main_loop_destroy(loop); 838 | my_pw_deinit(); 839 | } 840 | 841 | #define STR_HELPER(x) #x 842 | #define STR(x) STR_HELPER(x) 843 | 844 | static const char settings_dlg[] = 845 | "property \"PipeWire remote daemon name (empty for default)\" entry " CONFSTR_DDBPW_REMOTENAME " " STR(DDBPW_DEFAULT_REMOTENAME) ";\n" 846 | "property \"Custom properties (overrides existing ones):\" label l;\n" 847 | "property \"\" entry " CONFSTR_DDBPW_PROPS " \"\" ;\n" 848 | "property \"Use PipeWire volume control\" checkbox " CONFSTR_DDBPW_VOLUMECONTROL " " STR(DDBPW_DEFAULT_VOLUMECONTROL) ";\n" 849 | #ifdef ENABLE_BUFFER_OPTION 850 | "property \"Buffer length (ms)\" entry " CONFSTR_DDBPW_BUFLENGTH " " STR(DDBPW_DEFAULT_BUFLENGTH) ";\n" 851 | #endif 852 | ; 853 | 854 | 855 | static DB_output_t plugin = { 856 | .plugin.api_vmajor = DB_API_VERSION_MAJOR, 857 | .plugin.api_vminor = DB_API_VERSION_MINOR, 858 | .plugin.version_major = 0, 859 | .plugin.version_minor = 1, 860 | .plugin.flags = DDB_PLUGIN_FLAG_LOGGING, 861 | .plugin.type = DB_PLUGIN_OUTPUT, 862 | .plugin.id = PW_PLUGIN_ID, 863 | .plugin.name = "PipeWire output plugin dev", 864 | //.plugin.descr = "This is a new PipeWire plugin", 865 | .plugin.copyright = 866 | "Pipewire output plugin for DeaDBeeF Player\n" 867 | "Copyright (C) 2020-2021 Nicolai Syvertsen \n" 868 | "\n" 869 | "This program is free software; you can redistribute it and/or\n" 870 | "modify it under the terms of the GNU General Public License\n" 871 | "as published by the Free Software Foundation; either version 2\n" 872 | "of the License, or (at your option) any later version.\n" 873 | "\n" 874 | "This program is distributed in the hope that it will be useful,\n" 875 | "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" 876 | "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" 877 | "GNU General Public License for more details.\n" 878 | "\n" 879 | "You should have received a copy of the GNU General Public License\n" 880 | "along with this program; if not, write to the Free Software\n" 881 | "Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n", 882 | .plugin.website = "http://saivert.com", 883 | .plugin.start = ddbpw_plugin_start, 884 | .plugin.stop = ddbpw_plugin_stop, 885 | .plugin.configdialog = settings_dlg, 886 | .plugin.message = ddbpw_message, 887 | .init = ddbpw_init, 888 | .free = ddbpw_free, 889 | .setformat = ddbpw_setformat, 890 | .play = ddbpw_play, 891 | .stop = ddbpw_stop, 892 | .pause = ddbpw_pause, 893 | .unpause = ddbpw_unpause, 894 | .state = ddbpw_get_state, 895 | .enum_soundcards = ddbpw_enum_soundcards, 896 | .has_volume = DDBPW_DEFAULT_VOLUMECONTROL, 897 | }; 898 | -------------------------------------------------------------------------------- /pw_time_print.c: -------------------------------------------------------------------------------- 1 | struct pw_time time; 2 | pw_stream_get_time_n(data->stream, &time, sizeof(time)); 3 | printf("now %ld, rate %u/%u, ticks %lu, delay %ld, queued %lu, buffered %lu, queued_buffers %u, avail_buffers %u\r", 4 | time.now, time.rate.num ,time.rate.denom, time.ticks, time.delay, time.queued, time.buffered, time.queued_buffers, time.avail_buffers); 5 | fflush(stdout); 6 | --------------------------------------------------------------------------------