├── .gitignore ├── LICENSE ├── README.md ├── cavalcade ├── __init__.py ├── adata.py ├── autocolor.py ├── canvas.py ├── cava.py ├── cavapage.py ├── colordata.py ├── common.py ├── config.py ├── data │ ├── DefaultWallpaper.svg │ ├── cava.ini │ └── main.ini ├── drawing.py ├── gui │ ├── appmenu.ui │ ├── cavapage.glade │ ├── colors.glade │ ├── playerpage.glade │ ├── settings.ui │ ├── visualpage.glade │ └── winstate.ui ├── logger.py ├── mainapp.py ├── pixbuf.py ├── player.py ├── playerpage.py ├── run.py ├── settings.py ├── version.py └── visualpage.py ├── desktop ├── cavalcade.desktop └── cavalcade.svg ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore python cache 2 | **/__pycache__ 3 | **/*.pyc 4 | 5 | # Ignore setup env 6 | log.txt 7 | build/ 8 | dist/ 9 | .idea/ 10 | *.egg-info/ -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cavalcade 2 | 3 | Python wrapper for [C.A.V.A.](https://github.com/karlstav/cava) utility with his own drawing window, gui settings and basic audio player functions. 4 | 5 | ![](http://i.imgur.com/D6I21lL.png) 6 |

Video Demo

7 | 8 | #### Dependencies 9 | 10 | ###### Base 11 | * C.A.V.A. >= 0.6 12 | * GTK+ >=3.18 13 | * Python >=3.5 14 | * Cairo 15 | 16 | And all necessary python bindings e.g. python3-gi, python3-cairo. 17 | 18 | ###### Optional 19 | * GStreamer >=1.0 20 | * Python Pillow 21 | 22 | #### Installation 23 | This is pure python package, so you can try it without install 24 | ```bash 25 | $ git clone https://github.com/worron/cavalcade.git ~/cavalcade 26 | $ python3 ~/cavalcade/cavalcade/run.py 27 | ``` 28 | For proper install regular `pip` routine recommended 29 | ```bash 30 | $ git clone https://github.com/worron/cavalcade.git ~/cavalcade 31 | $ cd cavalcade 32 | $ pip install . 33 | ``` 34 | 35 | #### Usage 36 | To use spectrum audio visualizer launch cavalcade without any arguments. 37 | To use cavalcade as player launch it with list of files: 38 | ```bash 39 | $ cavalcade audio.mp3 40 | ``` 41 | Use help command to get list of all available arguments: 42 | ```bash 43 | $ cavalcade --help 44 | ``` 45 | 46 | Double click on window to show settings dialog. 47 | Program hotkeys can be set with user config file `~/.config/cavalcade/main.ini`. -------------------------------------------------------------------------------- /cavalcade/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worron/cavalcade/68ba5a2b2effd1c46b0568f4a27852689c2cdf32/cavalcade/__init__.py -------------------------------------------------------------------------------- /cavalcade/adata.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | import os 3 | import pickle 4 | 5 | from cavalcade.logger import logger, debuginfo 6 | 7 | 8 | class Storage: 9 | """Base class for saving data between sessions""" 10 | def __init__(self, mainapp, filename): 11 | self.path = os.path.expanduser("~/.local/share/cavalcade") 12 | if not os.path.exists(self.path): 13 | os.makedirs(self.path) 14 | self.store = os.path.join(self.path, filename) 15 | 16 | self._mainapp = mainapp 17 | 18 | 19 | class SavedColors(Storage): 20 | """Player session management helper""" 21 | def __init__(self, mainapp): 22 | super().__init__(mainapp, "colors") 23 | 24 | if os.path.isfile(self.store): 25 | with open(self.store, "rb") as fp: 26 | self.colors = pickle.load(fp) 27 | else: 28 | self.colors = {} 29 | 30 | logger.debug("Saved colors: %s", self.colors) 31 | 32 | @debuginfo() 33 | def add_color(self, file_, color): 34 | self.colors[file_] = color 35 | return self.colors 36 | 37 | @debuginfo() 38 | def delete_color(self, file_): 39 | if file_ in self.colors: 40 | del self.colors[file_] 41 | else: 42 | logger.warning("Wrong color database key %s" % file_) 43 | 44 | @debuginfo() 45 | def find_color(self, file_): 46 | return self.colors.get(file_) 47 | 48 | def save(self): 49 | """Save current custom colors""" 50 | with open(self.store, "wb") as fp: 51 | pickle.dump(self.colors, fp) 52 | logger.debug("Saved colors:\n%s", self.colors) 53 | 54 | 55 | class AudioData(Storage): 56 | """Player session management helper""" 57 | def __init__(self, mainapp): 58 | super().__init__(mainapp, "audio") 59 | 60 | if not os.path.isfile(self.store): 61 | with open(self.store, "wb") as fp: 62 | pickle.dump({"list": [], "queue": []}, fp) 63 | 64 | self.files = [] 65 | self.queue = None 66 | self.updated = False 67 | 68 | def load(self, args): 69 | """Get audio files from command arguments list""" 70 | audio, broken = [], [] 71 | for item in args: 72 | audio.append(item) if item.endswith(".mp3") else broken.append(item) 73 | 74 | if audio: 75 | self.files = audio 76 | self.updated = True 77 | if broken: 78 | logger.warning("Can't load this files:\n%s" % "\n".join(broken)) 79 | 80 | def save(self): 81 | """Save current playlist""" 82 | if self._mainapp.imported.gstreamer: 83 | with open(self.store, "r+b") as fp: 84 | playdata = {"list": self._mainapp.player.playlist, "queue": self._mainapp.player.playqueue} 85 | if playdata["list"]: 86 | logger.debug("File list to save:\n%s" % str(playdata)) 87 | pickle.dump(playdata, fp) 88 | else: 89 | logger.debug("No playlist was saved") 90 | 91 | def restore(self): 92 | """Restore playlist from previous session""" 93 | if os.path.isfile(self.store): 94 | with open(self.store, "rb") as fp: 95 | playdata = pickle.load(fp) 96 | else: 97 | playdata = None 98 | 99 | if playdata is not None: 100 | logger.debug("Restore audio files list:\n%s" % str(playdata["list"])) 101 | self.files = playdata["list"] 102 | self.queue = playdata["queue"] 103 | self.updated = True 104 | else: 105 | logger.warning("Can't restore previous player session") 106 | 107 | def send_to_player(self): 108 | """Update playlist""" 109 | if self.updated and self._mainapp.imported.gstreamer: 110 | self._mainapp.player.load_playlist(self.files, self.queue) 111 | self.updated = False 112 | -------------------------------------------------------------------------------- /cavalcade/autocolor.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | import io 3 | import multiprocessing 4 | import colorsys 5 | 6 | from gi.repository import GLib, Gdk 7 | from cavalcade.common import AttributeDict 8 | from operator import add 9 | from PIL import Image 10 | from cavalcade.logger import logger 11 | 12 | 13 | def bytes_to_file(bytes_): 14 | return io.BytesIO(bytes_) if bytes_ is not None else None 15 | 16 | 17 | class Cluster: 18 | """Group of color points""" 19 | 20 | # noinspection PyDefaultArgument 21 | def __init__(self, points = []): 22 | self.points = [] 23 | self.mass = 0 24 | for p in points: 25 | self.add(p) 26 | 27 | def add(self, point): 28 | """Add new point to group""" 29 | self.points.append(point) 30 | self.mass += point.count 31 | 32 | def get_color(self): 33 | """Calculate average color for group""" 34 | color = [0.0] * 3 35 | for point in self.points: 36 | color = map(add, color, [c * point.count for c in point.rgb]) 37 | return [(c / max(self.mass, 1)) for c in color] 38 | 39 | 40 | def get_points(img, limit): 41 | """Transform image to pack of color points""" 42 | points = [] 43 | w, h = img.size 44 | for count, color in img.getcolors(w * h): 45 | rgb = [c / 255 for c in color] 46 | hsv = colorsys.rgb_to_hsv(*rgb) 47 | if hsv[1] > limit["saturation_min"] and hsv[2] > limit["value_min"]: 48 | points.append(AttributeDict(rgb=rgb, hsv=hsv, count=count)) 49 | return points 50 | 51 | 52 | def allocate(points, n=16, window=4): 53 | """Split points to groups according there color""" 54 | band = 1 / n 55 | clusters = [Cluster() for _ in range(n)] 56 | for point in points: 57 | i = int(point.hsv[0] // band) 58 | clusters[i].add(point) 59 | clusters_l = clusters + clusters[:window - 1] 60 | bands = [clusters_l[i:i + window] for i in range(n)] 61 | rebanded = [Cluster(sum([c.points for c in band], [])) for band in bands] 62 | return rebanded 63 | 64 | 65 | class AutoColor: 66 | """Image color analyzer""" 67 | def __init__(self, mainapp): 68 | super().__init__() 69 | self._mainapp = mainapp 70 | self.config = mainapp.config 71 | self.process = None 72 | 73 | self.pc, self.cc = multiprocessing.Pipe() # fix this 74 | self.watcher = None 75 | 76 | self._mainapp.connect("tag-image-update", self.on_tag_image_update) 77 | self._mainapp.connect("default-image-update", self.on_default_image_update) 78 | self._mainapp.connect("image-source-switch", self.on_image_source_switch) 79 | self._mainapp.connect("autocolor-refresh", self.on_image_source_switch) 80 | 81 | # noinspection PyUnusedLocal 82 | def on_tag_image_update(self, sender, bytedata): 83 | """New image from mp3 tag""" 84 | if self.config["image"]["usetag"]: 85 | # dirty trick 86 | saved_color = self._mainapp.palette.find_color(self._mainapp.player.current) 87 | if saved_color is not None: 88 | rgba = Gdk.RGBA(*saved_color, self.config["color"]["autofg"].alpha) 89 | self._mainapp.emit("ac-update", rgba) 90 | else: 91 | file_ = bytes_to_file(bytedata) 92 | # noinspection PyTypeChecker 93 | self.color_update(file_) 94 | 95 | # noinspection PyUnusedLocal 96 | def on_image_source_switch(self, sender, usetag): 97 | """Update color from mp3 tag""" 98 | if usetag: 99 | file_ = bytes_to_file(self._mainapp.canvas.tag_image_bytedata) 100 | else: 101 | file_ = self.config["image"]["default"] 102 | self.color_update(file_) 103 | 104 | # noinspection PyUnusedLocal 105 | def on_default_image_update(self, sender, file_): 106 | """Update color from default image""" 107 | if not self.config["image"]["usetag"]: 108 | self.color_update(file_) 109 | 110 | @staticmethod 111 | def calculate(file_, options, conn): 112 | """Find the main color of image""" 113 | img = Image.open(file_) 114 | img.thumbnail((options["isize"][0], options["isize"][1])) 115 | 116 | points = get_points(img, options) 117 | clusters = allocate(points, options["bands"], options["window"]) 118 | selected = max(clusters, key=lambda x: x.mass) 119 | conn.send(selected.get_color()) 120 | 121 | def color_setup(self, conn, flag): 122 | """Read data from resent calculation and transform it to rgba color""" 123 | if flag == GLib.IO_IN: 124 | color_values = conn.recv() 125 | rgba = Gdk.RGBA(*color_values, self.config["color"]["autofg"].alpha) 126 | self._mainapp.emit("ac-update", rgba) 127 | return True 128 | else: 129 | logger.error("Autocolor multiprocessing error: connection was unexpectedly terminated") 130 | 131 | def color_update(self, file_): 132 | """Launch new calculation process with given image bytedata""" 133 | if file_ is None or isinstance(file_, str) and file_.endswith(".svg"): # fix this 134 | return 135 | 136 | if self.process is None or not self.process.is_alive(): 137 | if self.watcher is None: 138 | self.watcher = GLib.io_add_watch(self.pc, GLib.IO_IN | GLib.IO_HUP, self.color_setup) 139 | self.process = multiprocessing.Process( 140 | target=self.calculate, args=(file_, self.config["autocolor"], self.cc) 141 | ) 142 | self.process.start() 143 | else: 144 | logger.error("Autocolor threading error: previous process still running, refusing to start new one") 145 | -------------------------------------------------------------------------------- /cavalcade/canvas.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | 3 | from gi.repository import Gtk, Gdk, Gio, GLib 4 | import cavalcade.pixbuf as pixbuf 5 | 6 | from cavalcade.logger import logger 7 | from cavalcade.common import set_actions 8 | 9 | 10 | def bool_to_srt(*values): 11 | """Translate list of booleans to string""" 12 | return ";".join("1" if v else "" for v in values) 13 | 14 | 15 | # noinspection PyUnusedLocal 16 | class Canvas: 17 | """Main window manager""" 18 | def __init__(self, mainapp): 19 | self._mainapp = mainapp 20 | self.config = mainapp.config 21 | self.draw = mainapp.draw 22 | 23 | self.default_size = self.config["misc"]["dsize"] 24 | self.last_size = (-1, -1) 25 | self.tag_image_bytedata = None 26 | self.actions = {} 27 | 28 | # window setup 29 | self.overlay = Gtk.Overlay() 30 | self.image = Gtk.Image() 31 | self.scrolled = Gtk.ScrolledWindow() 32 | self.scrolled.add(self.image) 33 | 34 | self.overlay.add(self.scrolled) 35 | self.overlay.add_overlay(self.draw.area) 36 | 37 | self.va = self.scrolled.get_vadjustment() 38 | self.ha = self.scrolled.get_hadjustment() 39 | 40 | # actions 41 | self.actions["winstate"] = Gio.SimpleActionGroup() 42 | 43 | ialign_str = bool_to_srt(self.config["image"]["ha"], self.config["image"]["va"]) 44 | ialign_variant = GLib.Variant.new_string(ialign_str) 45 | ialign_action = Gio.SimpleAction.new_stateful("ialign", ialign_variant.get_type(), ialign_variant) 46 | ialign_action.connect("change-state", self._on_ialign) 47 | self.actions["winstate"].add_action(ialign_action) 48 | 49 | hint_variant = GLib.Variant.new_string(self.config["misc"]["hint"].value_nick.upper()) 50 | hint_action = Gio.SimpleAction.new_stateful("hint", hint_variant.get_type(), hint_variant) 51 | hint_action.connect("change-state", self._on_hint) 52 | self.actions["winstate"].add_action(hint_action) 53 | 54 | show_image_action = Gio.SimpleAction.new_stateful( 55 | "image", None, GLib.Variant.new_boolean(self.config["image"]["show"]) 56 | ) 57 | show_image_action.connect("change-state", self._on_show_image) 58 | self.actions["winstate"].add_action(show_image_action) 59 | 60 | for key, value in self.config["window"].items(): 61 | action = Gio.SimpleAction.new_stateful(key, None, GLib.Variant.new_boolean(value)) 62 | action.connect("change-state", self._on_winstate) 63 | self.actions["winstate"].add_action(action) 64 | 65 | # signals 66 | self._mainapp.connect("tag-image-update", self.on_tag_image_update) 67 | self._mainapp.connect("default-image-update", self.on_default_image_update) 68 | 69 | # cursor control 70 | self._is_cursor_hidden = False 71 | self._cursor_hide_timer = None 72 | self._cursor_hide_timeout = self.config["misc"]["cursor_hide_timeout"] * 1000 73 | self._launch_cursor_hide_timer() 74 | 75 | @property 76 | def ready(self): 77 | return hasattr(self, "window") 78 | 79 | def setup(self): 80 | """Init drawing window""" 81 | self.rebuild_window() 82 | # fix this 83 | if not self.config["image"]["show"]: 84 | self.overlay.remove(self.scrolled) 85 | 86 | def _launch_cursor_hide_timer(self): 87 | if self._cursor_hide_timer: 88 | GLib.source_remove(self._cursor_hide_timer) 89 | 90 | self._cursor_hide_timer = GLib.timeout_add(self._cursor_hide_timeout, self._hide_cursor) 91 | 92 | def _hide_cursor(self): 93 | window = self.window.get_window() 94 | 95 | if window: 96 | window.set_cursor(Gdk.Cursor(Gdk.CursorType.BLANK_CURSOR)) 97 | self._is_cursor_hidden = True 98 | self._cursor_hide_timer = None 99 | 100 | def _restore_cursor(self): 101 | window = self.window.get_window() 102 | 103 | if window: 104 | cursor = Gdk.Cursor.new_from_name(window.get_display(), 'default') 105 | window.set_cursor(cursor) 106 | self._is_cursor_hidden = False 107 | 108 | def _on_motion_notify_event(self, _widget, _event): 109 | if self._is_cursor_hidden: 110 | self._restore_cursor() 111 | 112 | self._launch_cursor_hide_timer() 113 | 114 | # action handlers 115 | def _on_ialign(self, action, value): 116 | action.set_state(value) 117 | state = [bool(s) for s in value.get_string().split(";")] 118 | self.config["image"]["ha"], self.config["image"]["va"] = state 119 | 120 | def _on_winstate(self, action, value): 121 | action.set_state(value) 122 | self.set_property(action.get_name(), value.get_boolean()) 123 | 124 | def _on_hint(self, action, value): 125 | """Set window type hint""" 126 | action.set_state(value) 127 | self.config["misc"]["hint"] = getattr(Gdk.WindowTypeHint, value.get_string()) 128 | self.rebuild_window() 129 | 130 | def _on_show_image(self, action, value): 131 | """Draw image background or solid paint""" 132 | action.set_state(value) 133 | show = value.get_boolean() 134 | 135 | if self.config["image"]["show"] != show: 136 | self.config["image"]["show"] = show 137 | if show: 138 | self.overlay.add(self.scrolled) 139 | self.rebuild_background() 140 | else: 141 | self.overlay.remove(self.scrolled) 142 | 143 | # Base window properties 144 | def _set_maximize(self, value): 145 | self.config["window"]["maximize"] = value 146 | action = self.window.maximize if value else self.window.unmaximize 147 | action() 148 | 149 | def _set_stick(self, value): 150 | self.config["window"]["stick"] = value 151 | action = self.window.stick if value else self.window.unstick 152 | action() 153 | 154 | def _set_below(self, value): 155 | self.config["window"]["below"] = value 156 | self.window.set_keep_below(value) 157 | 158 | def _set_skiptaskbar(self, value): 159 | self.config["window"]["skiptaskbar"] = value 160 | self.window.set_skip_taskbar_hint(value) 161 | 162 | def _set_winbyscreen(self, value): 163 | self.config["window"]["winbyscreen"] = value 164 | size = self._screen_size() if value else self.default_size 165 | self.window.move(0, 0) 166 | self.window.resize(*size) 167 | 168 | def _set_fullscreen(self, value): 169 | self.config["window"]["fullscreen"] = value 170 | action = self.window.fullscreen if value else self.window.unfullscreen 171 | action() 172 | 173 | def _set_imagebyscreen(self, value): 174 | """Resize background image to screen size despite current window size""" 175 | self.config["window"]["imagebyscreen"] = value 176 | self.rebuild_background() 177 | 178 | if self.config["image"]["va"]: 179 | self.va.set_upper(self.screen.get_height()) 180 | self.va.set_value(self.screen.get_height()) 181 | if self.config["image"]["ha"]: 182 | self.ha.set_upper(self.screen.get_width()) 183 | self.ha.set_value(self.screen.get_width()) 184 | 185 | def _set_bgpaint(self, value): 186 | """Use solid color or transparent background""" 187 | self.config["window"]["bgpaint"] = value 188 | rgba = self.config["color"]["bg"] if value else Gdk.RGBA(0, 0, 0, 0) 189 | self.set_bg_rgba(rgba) 190 | 191 | def _screen_size(self): 192 | """Get current screen size""" 193 | return self.screen.get_width(), self.screen.get_height() 194 | 195 | def rebuild_background(self): 196 | """Update background according current state""" 197 | size = self._screen_size() if self.config["window"]["imagebyscreen"] else self.last_size 198 | if not self.config["image"]["usetag"] or self.tag_image_bytedata is None: 199 | pb = pixbuf.from_file_at_scale(self.config["image"]["default"], *size) 200 | else: 201 | pb = pixbuf.from_bytes_at_scale(self.tag_image_bytedata, *size) 202 | self.image.set_from_pixbuf(pb) 203 | 204 | # noinspection PyUnusedLocal 205 | def _on_size_update(self, *args): 206 | """Update window state on size changes""" 207 | size = self.window.get_size() 208 | if self.last_size != size: 209 | self.last_size = size 210 | if self.config["image"]["show"]: 211 | if self.config["window"]["imagebyscreen"]: 212 | self.va.set_value(self.screen.get_height() if self.config["image"]["va"] else 0) 213 | self.ha.set_value(self.screen.get_width() if self.config["image"]["ha"] else 0) 214 | else: 215 | self.rebuild_background() 216 | 217 | def set_bg_rgba(self, rgba): 218 | """Set window background color""" 219 | self.window.override_background_color(Gtk.StateFlags.NORMAL, rgba) 220 | 221 | def rebuild_window(self): 222 | """ 223 | Recreate main window according current settings. 224 | This may be useful for update specific window properties. 225 | """ 226 | # destroy old window 227 | if self.ready: 228 | self.window.remove(self.overlay) 229 | self._mainapp.remove_window(self.window) 230 | self.window.destroy() 231 | 232 | # init new 233 | # noinspection PyAttributeOutsideInit 234 | self.window = Gtk.ApplicationWindow() 235 | # noinspection PyAttributeOutsideInit 236 | self.screen = self.window.get_screen() 237 | self.window.set_visual(self.screen.get_rgba_visual()) 238 | self._mainapp.add_window(self.window) 239 | 240 | self.window.set_default_size(*self.default_size) 241 | 242 | # set window state according config settings 243 | for name, value in self.config["window"].items(): 244 | self.set_property(name, value) 245 | self.window.set_type_hint(self.config["misc"]["hint"]) 246 | 247 | # set drawing widget 248 | self.window.add(self.overlay) 249 | 250 | # signals 251 | self.window.connect("delete-event", self._mainapp.close) 252 | self.draw.area.connect("button-press-event", self.on_click) 253 | self.draw.area.connect('motion-notify-event', self._on_motion_notify_event) 254 | self.window.connect("check-resize", self._on_size_update) 255 | 256 | self.draw.area.add_events(Gdk.EventMask.POINTER_MOTION_MASK) 257 | 258 | set_actions(self.actions, self.window) 259 | 260 | # show 261 | self.window.show_all() 262 | 263 | # noinspection PyUnusedLocal 264 | def on_click(self, widget, event): 265 | """Show settings window""" 266 | # noinspection PyProtectedMember 267 | if event.type == Gdk.EventType.BUTTON_PRESS: 268 | self.run_action("settings", "hide") 269 | elif event.type == Gdk.EventType._2BUTTON_PRESS: 270 | self.run_action("settings", "show") 271 | 272 | def run_action(self, group, name): 273 | """Activate action""" 274 | action = self.window.get_action_group(group) 275 | if action is not None: 276 | action.activate_action(name) 277 | 278 | def set_property(self, name, value): 279 | """Set window appearance property""" 280 | settler = "_set_%s" % name 281 | if hasattr(self, settler): 282 | getattr(self, settler)(value) 283 | else: 284 | logger.warning("Wrong window property '%s'" % name) 285 | 286 | # noinspection PyUnusedLocal 287 | def on_tag_image_update(self, sender, bytedata): 288 | """New image from mp3 tag""" 289 | self.tag_image_bytedata = bytedata 290 | self.rebuild_background() 291 | 292 | # noinspection PyUnusedLocal 293 | def on_default_image_update(self, sender, file_): 294 | """Update default background""" 295 | self.rebuild_background() 296 | -------------------------------------------------------------------------------- /cavalcade/cava.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | 3 | import os 4 | import struct 5 | import threading 6 | import subprocess 7 | 8 | from gi.repository import GLib 9 | from cavalcade.logger import logger 10 | 11 | 12 | class Cava: 13 | """ 14 | CAVA wrapper. 15 | Launch cava process with certain settings and read output. 16 | """ 17 | NONE = 0 18 | RUNNING = 1 19 | RESTARTING = 2 20 | CLOSING = 3 21 | 22 | def __init__(self, mainapp): 23 | self.cavaconfig = mainapp.cavaconfig 24 | self.path = self.cavaconfig["output"]["raw_target"] 25 | self.data_handler = mainapp.draw.update 26 | self.command = ["cava", "-p", self.cavaconfig.file] 27 | self.state = self.NONE 28 | 29 | self.env = dict(os.environ) 30 | self.env["LC_ALL"] = "en_US.UTF-8" # not sure if it's necessary 31 | 32 | is_16bit = self.cavaconfig["output"]["bit_format"] == "16bit" 33 | self.byte_type, self.byte_size, self.byte_norm = ("H", 2, 65535) if is_16bit else ("B", 1, 255) 34 | 35 | if not os.path.exists(self.path): 36 | os.mkfifo(self.path) 37 | 38 | def _run_process(self): 39 | logger.debug("Launching cava process...") 40 | try: 41 | self.process = subprocess.Popen( 42 | self.command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=self.env 43 | ) 44 | logger.debug("cava successfully launched!") 45 | self.state = self.RUNNING 46 | except Exception: 47 | logger.exception("Fail to launch cava") 48 | 49 | def _start_reader_thread(self): 50 | logger.debug("Activate cava stream handler") 51 | self.thread = threading.Thread(target=self._read_output) 52 | self.thread.daemon = True 53 | self.thread.start() 54 | 55 | def _read_output(self): 56 | fifo = open(self.path, "rb") 57 | chunk = self.byte_size * self.cavaconfig["general"]["bars"] # number of bytes for given format 58 | fmt = self.byte_type * self.cavaconfig["general"]["bars"] # pack of given format 59 | while True: 60 | data = fifo.read(chunk) 61 | if len(data) < chunk: 62 | break 63 | sample = [i / self.byte_norm for i in struct.unpack(fmt, data)] 64 | GLib.idle_add(self.data_handler, sample) 65 | fifo.close() 66 | GLib.idle_add(self._on_stop) 67 | 68 | def _on_stop(self, ): 69 | logger.debug("Cava stream handler deactivated") 70 | if self.state == self.RESTARTING: 71 | if not self.thread.isAlive(): 72 | self.start() 73 | else: 74 | logger.error("Can't restart cava, old handler still alive") 75 | elif self.state == self.RUNNING: 76 | self.state = self.NONE 77 | logger.error("Cava process was unexpectedly terminated.") 78 | # self.restart() # May cause infinity loop, need more check 79 | 80 | def start(self): 81 | """Launch cava""" 82 | self._start_reader_thread() 83 | self._run_process() 84 | 85 | def restart(self): 86 | """Restart cava process""" 87 | if self.state == self.RUNNING: 88 | logger.debug("Restarting cava process (normal mode) ...") 89 | self.state = self.RESTARTING 90 | if self.process.poll() is None: 91 | self.process.kill() 92 | elif self.state == self.NONE: 93 | logger.warning("Restarting cava process (after crash) ...") 94 | self.start() 95 | 96 | def close(self): 97 | """Stop cava process""" 98 | self.state = self.CLOSING 99 | if self.process.poll() is None: 100 | self.process.kill() 101 | os.remove(self.path) 102 | -------------------------------------------------------------------------------- /cavalcade/cavapage.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | from cavalcade.common import GuiBase 3 | from cavalcade.logger import logger 4 | from gi.repository import Gtk 5 | from cavalcade.common import AttributeDict 6 | 7 | 8 | class CavaPage(GuiBase): 9 | """CAVA setting page""" 10 | def __init__(self, mainapp): 11 | self._mainapp = mainapp 12 | elements = ( 13 | "mainbox", "restart_button", "bars_spinbutton", "sensitivity_spinbutton", "framerate_spinbutton", 14 | "lower_cutoff_freq_spinbutton", "higher_cutoff_freq_spinbutton", "gravity_spinbutton", 15 | "integral_spinbutton", "ignore_spinbutton", "monstercat_switch", "autosens_switch", "style_combobox", 16 | "eq_treeview", 17 | ) 18 | super().__init__("cavapage.glade", elements=elements) 19 | 20 | # set gui data 21 | self.gui["bars_spinbutton"].set_adjustment(Gtk.Adjustment(20, 20, 200, 1, 0, 0)) 22 | self.gui["framerate_spinbutton"].set_adjustment(Gtk.Adjustment(30, 12, 60, 1, 0, 0)) 23 | self.gui["sensitivity_spinbutton"].set_adjustment(Gtk.Adjustment(50, 10, 200, 5, 0, 0)) 24 | self.gui["ignore_spinbutton"].set_adjustment(Gtk.Adjustment(0, 0, 50, 1, 0, 0)) 25 | self.gui["gravity_spinbutton"].set_adjustment(Gtk.Adjustment(100, 0, 999, 10, 0, 0)) 26 | self.gui["integral_spinbutton"].set_adjustment(Gtk.Adjustment(50, 0, 100, 1, 0, 0)) 27 | self.gui["higher_cutoff_freq_spinbutton"].set_adjustment(Gtk.Adjustment(50, 50, 20000, 10, 0, 0)) 28 | self.gui["lower_cutoff_freq_spinbutton"].set_adjustment(Gtk.Adjustment(50, 50, 20000, 10, 0, 0)) 29 | 30 | # some gui constants 31 | self.OUTPUT_STYLE = ("mono", "stereo") 32 | self.EQ_STORE = AttributeDict(LABEL=0, VALUE=1) 33 | 34 | # setup base elements 35 | self.gui["restart_button"].connect("clicked", self.on_restart_button_click) 36 | self.int_sp_buttons = ( 37 | ("general", "framerate"), ("general", "bars"), ("general", "sensitivity"), 38 | ("general", "higher_cutoff_freq"), ("general", "lower_cutoff_freq"), ("smoothing", "ignore"), 39 | ("smoothing", "integral"), ("smoothing", "gravity") 40 | ) 41 | self.float_sp_buttons = () 42 | self.bool_switches = (("smoothing", "monstercat"), ("general", "autosens")) 43 | 44 | for section, key in self.int_sp_buttons + self.float_sp_buttons: 45 | self.gui[key + "_spinbutton"].set_value(self._mainapp.cavaconfig[section][key]) 46 | 47 | for section, key in self.bool_switches: 48 | self.gui[key + "_switch"].set_active(self._mainapp.cavaconfig[section][key]) 49 | 50 | self.gui["style_combobox"].set_active(self.OUTPUT_STYLE.index(self._mainapp.cavaconfig["output"]["channels"])) 51 | 52 | # setup equalizer 53 | self.eq_store = Gtk.ListStore(str, float) 54 | self.gui['renderer_spin'] = Gtk.CellRendererSpin( 55 | digits=2, editable=True, adjustment=Gtk.Adjustment(1, 0.1, 1, 0.1, 0, 0) 56 | ) 57 | self.gui['renderer_spin'].connect("edited", self.on_eq_edited) 58 | 59 | column1 = Gtk.TreeViewColumn("Frequency Bands", Gtk.CellRendererText(), text=self.EQ_STORE.LABEL) 60 | column1.set_expand(True) 61 | column2 = Gtk.TreeViewColumn("Value", self.gui['renderer_spin'], text=self.EQ_STORE.VALUE) 62 | column2.set_min_width(200) 63 | 64 | self.gui['eq_treeview'].append_column(column1) 65 | self.gui['eq_treeview'].append_column(column2) 66 | self.gui['eq_treeview'].set_model(self.eq_store) 67 | 68 | for i, value in enumerate(self._mainapp.cavaconfig["eq"]): 69 | self.eq_store.append(["Frequency band %d" % (i + 1), value]) 70 | 71 | # gui handlers 72 | # noinspection PyUnusedLocal 73 | def on_restart_button_click(self, button): 74 | if self._mainapp.cavaconfig.is_fallback: 75 | logger.error("This changes not permitted while system config file active.") 76 | return 77 | 78 | # read settings from widgets 79 | for section, key in self.int_sp_buttons: 80 | self._mainapp.cavaconfig[section][key] = int(self.gui[key + "_spinbutton"].get_value()) 81 | 82 | for section, key in self.float_sp_buttons: 83 | self._mainapp.cavaconfig[section][key] = self.gui[key + "_spinbutton"].get_value() 84 | 85 | for section, key in self.bool_switches: 86 | self._mainapp.cavaconfig[section][key] = self.gui[key + "_switch"].get_active() 87 | 88 | self._mainapp.cavaconfig["output"]["channels"] = self.gui["style_combobox"].get_active_text().lower() 89 | self._mainapp.cavaconfig["eq"] = [line[self.EQ_STORE.VALUE] for line in self.eq_store] 90 | 91 | # update settings with current data 92 | self._mainapp.cavaconfig.write_data() 93 | self._mainapp.cava.restart() 94 | self._mainapp.draw.size_update() 95 | 96 | # noinspection PyUnusedLocal 97 | def on_eq_edited(self, widget, path, text): 98 | self.eq_store[path][self.EQ_STORE.VALUE] = float(text) 99 | -------------------------------------------------------------------------------- /cavalcade/colordata.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | from gi.repository import Gtk, Pango, GdkPixbuf 3 | from cavalcade.common import GuiBase, TreeViewHolder, AttributeDict 4 | 5 | # TODO: make color list update on every new color added? 6 | 7 | 8 | class ColorsWindow(GuiBase): 9 | """User saved color list window""" 10 | def __init__(self, mainapp): 11 | elements = ( 12 | "window", "colors-treeview", "colors-searchentry", "colors-selection", "color-delete-button" 13 | ) 14 | super().__init__("colors.glade", elements=elements) 15 | 16 | self._mainapp = mainapp 17 | self.search_text = None 18 | 19 | # some gui constants 20 | self.COLOR_STORE = AttributeDict(INDEX=0, FILE=1, COLOR=2, ICON=3) 21 | self.PB = AttributeDict(bits=8, width=64, height=16, column_width = 88) 22 | 23 | # colors view setup 24 | self.treeview = self.gui["colors-treeview"] 25 | self.treelock = TreeViewHolder(self.treeview) 26 | 27 | for i, title in enumerate(("Index", "File", "Color", "Icon")): 28 | if i != self.COLOR_STORE.ICON: 29 | column = Gtk.TreeViewColumn(title, Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.START), text=i) 30 | column.set_resizable(True) 31 | column.set_expand(True) 32 | else: 33 | column = Gtk.TreeViewColumn(title, Gtk.CellRendererPixbuf(width=self.PB.column_width), pixbuf=i) 34 | 35 | self.treeview.append_column(column) 36 | column.set_visible(i in (self.COLOR_STORE.FILE, self.COLOR_STORE.ICON)) 37 | 38 | self.store = Gtk.ListStore(int, str, str, GdkPixbuf.Pixbuf) 39 | self.store_filter = self.store.filter_new() 40 | self.store_filter.set_visible_func(self.colors_filter_func) 41 | self.search_text = None 42 | 43 | self.treeview.set_model(self.store_filter) 44 | 45 | # accelerators 46 | self.accelerators = Gtk.AccelGroup() 47 | self.gui["window"].add_accel_group(self.accelerators) 48 | self.accelerators.connect(*Gtk.accelerator_parse("Escape"), Gtk.AccelFlags.VISIBLE, self.hide) 49 | 50 | # signals 51 | self.gui["window"].connect("delete-event", self.hide) 52 | self.gui["colors-searchentry"].connect("activate", self.on_search_active) 53 | self.gui["colors-searchentry"].connect("icon-release", self.on_search_reset) 54 | self.gui["color-delete-button"].connect("clicked", self.on_color_delete_button_click) 55 | 56 | def rebuild_store(self, data): 57 | """Update colors store""" 58 | with self.treelock: 59 | self.store.clear() 60 | 61 | for i, (file_, color) in enumerate(data.items()): 62 | pixbuf = GdkPixbuf.Pixbuf.new( 63 | GdkPixbuf.Colorspace.RGB, False, 64 | self.PB.bits, self.PB.width, self.PB.height 65 | ) 66 | pixbuf.fill(int("%02X%02X%02XFF" % tuple(int(i*255) for i in color), 16)) # fix this 67 | self.store.append([i, file_, "%.2f %.2f %.2f" % color, pixbuf]) 68 | 69 | # noinspection PyUnusedLocal 70 | def colors_filter_func(self, model, treeiter, data): 71 | """Function to filter current color list by search text""" 72 | if not self.search_text: 73 | return True 74 | else: 75 | return self.search_text.lower() in model[treeiter][self.COLOR_STORE.FILE].lower() 76 | 77 | # GUI handlers 78 | # noinspection PyUnusedLocal 79 | def on_search_active(self, *args): 80 | self.search_text = self.gui["colors-searchentry"].get_text() 81 | self.store_filter.refilter() 82 | 83 | # noinspection PyUnusedLocal 84 | def on_search_reset(self, *args): 85 | self.gui["colors-searchentry"].set_text("") 86 | self.on_search_active() 87 | 88 | # noinspection PyUnusedLocal 89 | def on_color_delete_button_click(self, *args): 90 | model, sel = self.gui["colors-selection"].get_selected() 91 | if sel is not None: 92 | file_ = model[sel][self.COLOR_STORE.FILE] 93 | self._mainapp.palette.delete_color(file_) 94 | self.rebuild_store(self._mainapp.palette.colors) 95 | 96 | # noinspection PyUnusedLocal 97 | def hide(self, *args): 98 | """Hide colors window""" 99 | self.gui["window"].hide() 100 | return True 101 | 102 | # noinspection PyUnusedLocal 103 | def show(self, *args): 104 | """Show colors window""" 105 | self.rebuild_store(self._mainapp.palette.colors) 106 | self.gui["window"].show_all() 107 | -------------------------------------------------------------------------------- /cavalcade/common.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | import os 3 | import gi 4 | from gi.repository import Gtk, Gdk 5 | from cavalcade.logger import logger 6 | 7 | WINDOW_HINTS = ("NORMAL", "DIALOG", "SPLASHSCREEN", "DOCK", "DESKTOP") 8 | 9 | 10 | def import_optional(): 11 | """Safe module import""" 12 | success = AttributeDict() 13 | try: 14 | gi.require_version('Gst', '1.0') 15 | from gi.repository import Gst # noqa: F401 16 | success.gstreamer = True 17 | except Exception: 18 | success.gstreamer = False 19 | logger.warning("Fail to import Gstreamer module") 20 | 21 | try: 22 | from PIL import Image # noqa: F401 23 | success.pillow = True 24 | except Exception: 25 | success.pillow = False 26 | logger.warning("Fail to import Pillow module") 27 | 28 | return success 29 | 30 | 31 | def set_actions(action_pack, widget): 32 | """Set actions groups from dictionary to widget""" 33 | for key, value in action_pack.items(): 34 | widget.insert_action_group(key, value) 35 | 36 | 37 | def name_from_file(file_): 38 | """Extract file name from full path""" 39 | return os.path.splitext(os.path.basename(file_))[0] 40 | 41 | 42 | def gtk_open_file(parent, filter_=None): 43 | """Gtk open file dialog""" 44 | dialog = Gtk.FileChooserDialog( 45 | "Select image file", parent, Gtk.FileChooserAction.OPEN, 46 | (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK) 47 | ) 48 | 49 | if filter_ is not None: 50 | dialog.add_filter(filter_) 51 | 52 | response = dialog.run() 53 | if response != Gtk.ResponseType.OK: 54 | is_ok, file_ = False, None 55 | else: 56 | is_ok = True 57 | file_ = dialog.get_filename() 58 | 59 | dialog.destroy() 60 | return is_ok, file_ 61 | 62 | 63 | class GuiBase: 64 | """Base for Gtk widget set created with builder""" 65 | path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "gui") 66 | 67 | def __init__(self, *files, elements=tuple()): 68 | self.builder = Gtk.Builder() 69 | for file_ in files: 70 | self.builder.add_from_file(os.path.join(self.path, file_)) 71 | self.gui = {element: self.builder.get_object(element) for element in elements} 72 | 73 | 74 | class AttributeDict(dict): 75 | """Dictionary with keys as attributes. Does nothing but easy reading""" 76 | def __getattr__(self, attr): 77 | return self[attr] 78 | 79 | def __setattr__(self, attr, value): 80 | self[attr] = value 81 | 82 | 83 | class TreeViewHolder: 84 | """Disconnect treeview store for rebuild""" 85 | def __init__(self, treeview): 86 | self.treeview = treeview 87 | 88 | def __enter__(self): 89 | self.store = self.treeview.get_model() 90 | self.treeview.set_model(None) 91 | 92 | def __exit__(self, type_, value, traceback): 93 | self.treeview.set_model(self.store) 94 | 95 | 96 | class AccelCheck: 97 | def __contains__(self, item): 98 | key, mod = Gtk.accelerator_parse(item) 99 | return any((key != 0, mod != Gdk.ModifierType(0))) 100 | -------------------------------------------------------------------------------- /cavalcade/config.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | 3 | import os 4 | import shutil 5 | 6 | from configparser import ConfigParser 7 | from gi.repository import Gdk 8 | from cavalcade.logger import logger 9 | from cavalcade.common import AttributeDict, WINDOW_HINTS, AccelCheck 10 | 11 | GTK_WINDOW_TYPE_HINTS = [getattr(Gdk.WindowTypeHint, hint) for hint in WINDOW_HINTS] 12 | DEFAULT_WALLPAPER_FILE = "DefaultWallpaper.svg" 13 | accel = AccelCheck() 14 | 15 | 16 | def str_to_rgba(hex_): 17 | """Translate color from hex string to Gdk.RGBA""" 18 | pure_hex = hex_.lstrip("#") 19 | nums = [int(pure_hex[i:i + 2], 16) / 255.0 for i in range(0, 7, 2)] 20 | return Gdk.RGBA(*nums) 21 | 22 | 23 | def rgba_to_str(rgba): 24 | """Translate color from Gdk.RGBA to hex format""" 25 | return "#%02X%02X%02X%02X" % tuple(int(getattr(rgba, name) * 255) for name in ("red", "green", "blue", "alpha")) 26 | 27 | 28 | class ConfigBase(dict): 29 | """Base for config manager""" 30 | system_location = (os.path.join(os.path.dirname(os.path.abspath(__file__)), "data"),) 31 | path = os.path.expanduser("~/.config/cavalcade") 32 | 33 | def __init__(self, name, pattern=None): 34 | super().__init__() 35 | self.name = name 36 | self.pattern = pattern if pattern is not None else {} 37 | self.is_fallback = False 38 | 39 | # read functions 40 | self.reader = { 41 | int: lambda section, option: self.parser.getint(section, option), 42 | bool: lambda section, option: self.parser.getboolean(section, option), 43 | str: lambda section, option: self.parser.get(section, option), 44 | float: lambda section, option: self.parser.getfloat(section, option), 45 | "ilist": lambda section, option: [int(v.strip()) for v in self.parser.get(section, option).split(";")], 46 | "hint": lambda section, option: getattr(Gdk.WindowTypeHint, self.parser.get(section, option)), 47 | "accel": lambda section, option: self.parser.get(section, option), 48 | Gdk.RGBA: lambda section, option: str_to_rgba(self.parser.get(section, option)), 49 | } 50 | 51 | # write functions 52 | self.writer = { 53 | int: lambda value: str(value), 54 | bool: lambda value: str(int(value)), 55 | str: lambda value: value, 56 | float: lambda value: "{:.2f}".format(value), 57 | "ilist": lambda value: ";".join(str(i) for i in value), 58 | "hint": lambda value: value.value_nick.upper(), 59 | "accel": lambda value: value, 60 | Gdk.RGBA: lambda value: rgba_to_str(value), 61 | } 62 | 63 | # init 64 | self._init_config_file() 65 | self._load_config_file() 66 | 67 | def _init_config_file(self): 68 | """Setup user config directory and file""" 69 | for path in self.system_location: 70 | candidate = os.path.join(path, self.name) 71 | if os.path.isfile(candidate): 72 | self.defconfig = candidate 73 | break 74 | 75 | if not os.path.exists(self.path): 76 | os.makedirs(self.path) 77 | 78 | self.file = os.path.join(self.path, self.name) 79 | 80 | if not os.path.isfile(self.file): 81 | shutil.copyfile(self.defconfig, self.file) 82 | logger.info("New configuration file was created:\n%s" % self.file) 83 | 84 | def _load_config_file(self): 85 | """Read raw config data""" 86 | self.parser = ConfigParser() 87 | try: 88 | self.parser.read(self.file) 89 | self.read_data() 90 | logger.debug("User config '%s' successfully loaded." % self.name) 91 | except Exception: 92 | self.is_fallback = True 93 | logger.exception("Fail to read '%s' user config:" % self.name) 94 | logger.info("Trying with default config...") 95 | self.parser.read(self.defconfig) 96 | self.read_data() 97 | logger.debug("Default config '%s' successfully loaded." % self.name) 98 | 99 | def read_data(self): 100 | """Transform raw config data to user specified types""" 101 | for section in self.pattern.keys(): 102 | self[section] = dict() 103 | for option, pattern in self.pattern[section].items(): 104 | reader = self.reader[pattern.type] 105 | self[section][option] = reader(section, option) 106 | if "valid" in pattern and self[section][option] not in pattern.valid: 107 | raise Exception("Bad value for '%s' in '%s'" % (option, section)) 108 | 109 | def write_data(self): 110 | """Transform user specified data to raw config parser strings""" 111 | for section in self.pattern.keys(): 112 | for option, pattern in self.pattern[section].items(): 113 | writer = self.writer[pattern.type] 114 | self.parser[section][option] = writer(self[section][option]) 115 | 116 | def save_data(self): 117 | """Save settings to file""" 118 | with open(self.file, 'w') as configfile: 119 | self.parser.write(configfile) 120 | 121 | 122 | class CavaConfig(ConfigBase): 123 | """CAVA config manager""" 124 | def __init__(self): 125 | super().__init__( 126 | "cava.ini", dict( 127 | general = dict( 128 | bars = AttributeDict(type=int), 129 | sensitivity = AttributeDict(type=int), 130 | framerate = AttributeDict(type=int), 131 | lower_cutoff_freq = AttributeDict(type=int), 132 | higher_cutoff_freq = AttributeDict(type=int), 133 | autosens = AttributeDict(type=bool), 134 | ), 135 | output = dict( 136 | method = AttributeDict(type=str, valid=["raw"]), 137 | raw_target = AttributeDict(type=str), 138 | channels = AttributeDict(type=str), 139 | bit_format = AttributeDict(type=str, valid=["16bit", "8bit"]), 140 | ), 141 | smoothing = dict( 142 | gravity = AttributeDict(type=int), 143 | integral = AttributeDict(type=int), 144 | ignore = AttributeDict(type=int), 145 | monstercat = AttributeDict(type=bool), 146 | ), 147 | ) 148 | ) 149 | 150 | def read_data(self): 151 | super().read_data() 152 | self["eq"] = [float(v) for v in self.parser["eq"].values()] 153 | 154 | def write_data(self): 155 | super().write_data() 156 | 157 | for i, key in enumerate(self.parser["eq"].keys()): 158 | self.parser["eq"][key] = "{:.2f}".format(self["eq"][i]) 159 | 160 | self.save_data() 161 | 162 | 163 | class MainConfig(ConfigBase): 164 | """Main application config manager""" 165 | def __init__(self): 166 | super().__init__( 167 | "main.ini", dict( 168 | draw = dict( 169 | padding = AttributeDict(type=int), 170 | zero = AttributeDict(type=int), 171 | silence = AttributeDict(type=int), 172 | scale = AttributeDict(type=float), 173 | ), 174 | color = dict( 175 | fg = AttributeDict(type=Gdk.RGBA), 176 | autofg = AttributeDict(type=Gdk.RGBA), 177 | bg = AttributeDict(type=Gdk.RGBA), 178 | auto = AttributeDict(type=bool), 179 | ), 180 | offset = dict( 181 | left = AttributeDict(type=int), 182 | right = AttributeDict(type=int), 183 | top = AttributeDict(type=int), 184 | bottom = AttributeDict(type=int), 185 | ), 186 | window = dict( 187 | maximize = AttributeDict(type=bool), 188 | below = AttributeDict(type=bool), 189 | stick = AttributeDict(type=bool), 190 | winbyscreen = AttributeDict(type=bool), 191 | imagebyscreen = AttributeDict(type=bool), 192 | bgpaint = AttributeDict(type=bool), 193 | fullscreen = AttributeDict(type=bool), 194 | skiptaskbar = AttributeDict(type=bool), 195 | ), 196 | image = dict( 197 | show = AttributeDict(type=bool), 198 | usetag = AttributeDict(type=bool), 199 | va = AttributeDict(type=bool), 200 | ha = AttributeDict(type=bool), 201 | default = AttributeDict(type=str) 202 | ), 203 | autocolor = dict( 204 | bands = AttributeDict(type=int), 205 | window = AttributeDict(type=int), 206 | saturation_min = AttributeDict(type=float), 207 | value_min = AttributeDict(type=float), 208 | isize = AttributeDict(type="ilist"), 209 | ), 210 | player = dict( 211 | volume = AttributeDict(type=float), 212 | shuffle = AttributeDict(type=bool), 213 | showqueue = AttributeDict(type=bool), 214 | ), 215 | misc = dict( 216 | hint = AttributeDict(type="hint", valid=GTK_WINDOW_TYPE_HINTS), 217 | dsize = AttributeDict(type="ilist"), 218 | cursor_hide_timeout = AttributeDict(type=int), 219 | 220 | ), 221 | keys = dict( 222 | exit = AttributeDict(type="accel", valid=accel), 223 | next = AttributeDict(type="accel", valid=accel), 224 | play = AttributeDict(type="accel", valid=accel), 225 | show = AttributeDict(type="accel", valid=accel), 226 | hide = AttributeDict(type="accel", valid=accel), 227 | ), 228 | ) 229 | ) 230 | 231 | def read_data(self): 232 | super().read_data() 233 | self._validate_default_bg() 234 | 235 | def _validate_default_bg(self): 236 | if not self["image"]["default"]: 237 | logger.info("Default wallpaper not defined, setting config option to fallback value.") 238 | self._set_fallback_bg() 239 | elif not os.path.isfile(self["image"]["default"]): 240 | logger.warning("Default wallpaper file not valid, resetting config option to fallback value.") 241 | self._set_fallback_bg() 242 | 243 | def _set_fallback_bg(self): 244 | self["image"]["default"] = os.path.join(os.path.dirname(self.defconfig), DEFAULT_WALLPAPER_FILE) 245 | 246 | def write_data(self): 247 | super().write_data() 248 | self.save_data() 249 | -------------------------------------------------------------------------------- /cavalcade/data/DefaultWallpaper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /cavalcade/data/cava.ini: -------------------------------------------------------------------------------- 1 | [general] 2 | bars = 40 3 | sensitivity = 30 4 | framerate = 60 5 | lower_cutoff_freq = 50 6 | higher_cutoff_freq = 10000 7 | autosens = 1 8 | 9 | # these parameters should not be changed 10 | # bar width and spacing goes wrong with raw output 11 | bar_width = 1 12 | bar_spacing = 0 13 | 14 | [output] 15 | method = raw 16 | raw_target = /tmp/cava.fifo 17 | bit_format = 16bit 18 | channels = stereo 19 | 20 | [smoothing] 21 | gravity = 200 22 | integral = 70 23 | ignore = 0 24 | monstercat = 1 25 | 26 | [eq] 27 | 1 = 1.00 28 | 2 = 1.00 29 | 3 = 1.00 30 | 4 = 1.00 31 | 5 = 1.00 32 | 6 = 1.00 33 | 7 = 1.00 34 | 8 = 1.00 35 | 9 = 1.00 -------------------------------------------------------------------------------- /cavalcade/data/main.ini: -------------------------------------------------------------------------------- 1 | [draw] 2 | padding = 5 3 | scale = 1 4 | zero = 4 5 | silence = 10 6 | 7 | [color] 8 | fg = #80808080 9 | autofg = #808080B0 10 | bg = #000000FF 11 | auto = 0 12 | 13 | [offset] 14 | left = 5 15 | right = 5 16 | top = 5 17 | bottom = 5 18 | 19 | [window] 20 | maximize = 0 21 | below = 0 22 | stick = 0 23 | winbyscreen = 0 24 | imagebyscreen = 0 25 | bgpaint = 1 26 | fullscreen = 0 27 | skiptaskbar = 0 28 | 29 | [image] 30 | show = 1 31 | usetag = 1 32 | default = 33 | va = 0 34 | ha = 0 35 | 36 | [autocolor] 37 | bands = 256 38 | window = 8 39 | saturation_min = 0.25 40 | value_min = 0.25 41 | isize = 160;90 42 | 43 | [misc] 44 | hint = NORMAL 45 | dsize = 1280;720 46 | cursor_hide_timeout = 3 47 | 48 | [player] 49 | volume = 0.50 50 | shuffle = 0 51 | showqueue = 0 52 | 53 | [keys] 54 | exit = q 55 | next = n 56 | play = space 57 | hide = Escape 58 | show = i 59 | -------------------------------------------------------------------------------- /cavalcade/drawing.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | from gi.repository import Gtk, Gdk 3 | from cavalcade.common import AttributeDict 4 | 5 | 6 | class Spectrum: 7 | """Spectrum drawing""" 8 | def __init__(self, config, cavaconfig): 9 | self.silence_value = 0 10 | self.config = config 11 | self.cavaconfig = cavaconfig 12 | self.audio_sample = [] 13 | self.color = None 14 | 15 | self.area = Gtk.DrawingArea() 16 | self.area.connect("draw", self.redraw) 17 | self.area.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) 18 | 19 | self.sizes = AttributeDict() 20 | self.sizes.area = AttributeDict() 21 | self.sizes.bar = AttributeDict() 22 | 23 | self.area.connect("configure-event", self.size_update) 24 | self.color_update() 25 | 26 | def is_silence(self, value): 27 | """Check if volume level critically low during last iterations""" 28 | self.silence_value = 0 if value > 0 else self.silence_value + 1 29 | return self.silence_value > self.config["draw"]["silence"] 30 | 31 | def update(self, data): 32 | """Audio data processing""" 33 | self.audio_sample = data 34 | if not self.is_silence(self.audio_sample[0]): 35 | self.area.queue_draw() 36 | elif self.silence_value == (self.config["draw"]["silence"] + 1): 37 | self.audio_sample = [0] * self.sizes.number 38 | self.area.queue_draw() 39 | 40 | # noinspection PyUnusedLocal 41 | def redraw(self, widget, cr): 42 | """Draw spectrum graph""" 43 | cr.set_source_rgba(*self.color) 44 | 45 | dx = self.config["offset"]["left"] 46 | for i, value in enumerate(self.audio_sample): 47 | width = self.sizes.bar.width + int(i < self.sizes.wcpi) 48 | height = max(self.sizes.bar.height * min(self.config["draw"]["scale"] * value, 1), self.sizes.zero) 49 | cr.rectangle(dx, self.sizes.area.height, width, - height) 50 | dx += width + self.sizes.padding 51 | cr.fill() 52 | 53 | # noinspection PyUnusedLocal 54 | def size_update(self, *args): 55 | """Update drawing geometry""" 56 | self.sizes.number = self.cavaconfig["general"]["bars"] 57 | self.sizes.padding = self.config["draw"]["padding"] 58 | self.sizes.zero = self.config["draw"]["zero"] 59 | 60 | self.sizes.area.width = self.area.get_allocated_width() - self.config["offset"]["right"] 61 | self.sizes.area.height = self.area.get_allocated_height() - self.config["offset"]["bottom"] 62 | 63 | tw = (self.sizes.area.width - self.config["offset"]["left"]) - self.sizes.padding * (self.sizes.number - 1) 64 | self.sizes.bar.width = max(int(tw / self.sizes.number), 1) 65 | self.sizes.bar.height = self.sizes.area.height - self.config["offset"]["top"] 66 | self.sizes.wcpi = tw % self.sizes.number # width correction point index 67 | 68 | def color_update(self): 69 | """Set drawing color according current settings""" 70 | self.color = self.config["color"]["autofg"] if self.config["color"]["auto"] else self.config["color"]["fg"] 71 | -------------------------------------------------------------------------------- /cavalcade/gui/appmenu.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | player.play 7 | Play/Pause 8 | 9 | 10 | player.next 11 | Next Track 12 | 13 |
14 |
15 | 16 | settings.colors 17 | Saved Colors 18 | 19 |
20 |
21 | 22 | settings.hide 23 | Hide 24 | 25 | 26 | app.quit 27 | Quit 28 | 29 |
30 |
31 |
32 | 33 | -------------------------------------------------------------------------------- /cavalcade/gui/cavapage.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | True 7 | False 8 | vertical 9 | 10 10 | 11 | 12 | True 13 | False 14 | 6 15 | True 16 | 8 17 | 18 18 | True 19 | 20 | 21 | True 22 | False 23 | 12 24 | 25 | 26 | True 27 | False 28 | Bars 29 | 30 | 31 | False 32 | True 33 | 0 34 | 35 | 36 | 37 | 38 | 100 39 | True 40 | True 41 | The number of bars. 42 | 1,0 43 | 1 44 | 45 | 46 | False 47 | True 48 | end 49 | 1 50 | 51 | 52 | 53 | 54 | 0 55 | 0 56 | 57 | 58 | 59 | 60 | True 61 | False 62 | 12 63 | 64 | 65 | True 66 | False 67 | Sensitivity 68 | 69 | 70 | False 71 | True 72 | 0 73 | 74 | 75 | 76 | 77 | 100 78 | True 79 | True 80 | Sensitivity level. 81 | 1,0 82 | 1 83 | 84 | 85 | False 86 | True 87 | end 88 | 1 89 | 90 | 91 | 92 | 93 | 1 94 | 0 95 | 96 | 97 | 98 | 99 | True 100 | False 101 | 12 102 | 103 | 104 | True 105 | False 106 | Framerate 107 | 108 | 109 | False 110 | True 111 | 0 112 | 113 | 114 | 115 | 116 | 100 117 | True 118 | True 119 | Framerate. 120 | 10,0 121 | 10 122 | 123 | 124 | False 125 | True 126 | end 127 | 1 128 | 129 | 130 | 131 | 132 | 0 133 | 1 134 | 135 | 136 | 137 | 138 | True 139 | False 140 | 12 141 | 142 | 143 | True 144 | False 145 | Ignore 146 | 147 | 148 | False 149 | True 150 | 0 151 | 152 | 153 | 154 | 155 | 100 156 | True 157 | True 158 | In bar height, bars that would have been lower that this will not be drawn. 159 | 9,00 160 | 161 | 162 | False 163 | True 164 | end 165 | 1 166 | 167 | 168 | 169 | 170 | 1 171 | 1 172 | 173 | 174 | 175 | 176 | False 177 | True 178 | 1 179 | 180 | 181 | 182 | 183 | True 184 | False 185 | 186 | 187 | False 188 | True 189 | 2 190 | 191 | 192 | 193 | 194 | True 195 | False 196 | 8 197 | 18 198 | True 199 | 200 | 201 | True 202 | False 203 | 12 204 | 205 | 206 | True 207 | False 208 | Integral 209 | 210 | 211 | False 212 | True 213 | 0 214 | 215 | 216 | 217 | 218 | 100 219 | True 220 | True 221 | Multiplier for the integral smoothing calculations. 222 | 9,00 223 | 9 224 | 225 | 226 | False 227 | True 228 | end 229 | 1 230 | 231 | 232 | 233 | 234 | 1 235 | 1 236 | 237 | 238 | 239 | 240 | True 241 | False 242 | 12 243 | 244 | 245 | True 246 | False 247 | Gravity 248 | 249 | 250 | False 251 | True 252 | 0 253 | 254 | 255 | 256 | 257 | 100 258 | True 259 | True 260 | Set gravity multiplier for "drop off". Higher values means bars will drop faster. 261 | 12,00 262 | 263 | 264 | False 265 | True 266 | end 267 | 1 268 | 269 | 270 | 271 | 272 | 1 273 | 0 274 | 275 | 276 | 277 | 278 | True 279 | False 280 | 12 281 | 282 | 283 | True 284 | False 285 | Monstercat 286 | 287 | 288 | False 289 | True 290 | 0 291 | 292 | 293 | 294 | 295 | True 296 | True 297 | Use the monstercat filter. 298 | center 299 | 300 | 301 | False 302 | True 303 | end 304 | 1 305 | 306 | 307 | 308 | 309 | 0 310 | 0 311 | 312 | 313 | 314 | 315 | True 316 | False 317 | 12 318 | 319 | 320 | True 321 | False 322 | Autosens 323 | 324 | 325 | False 326 | True 327 | 0 328 | 329 | 330 | 331 | 332 | True 333 | True 334 | Autosens will attempt to decrease sensitivity if cava peaks. 335 | center 336 | 337 | 338 | False 339 | True 340 | end 341 | 1 342 | 343 | 344 | 345 | 346 | 0 347 | 1 348 | 349 | 350 | 351 | 352 | False 353 | True 354 | 3 355 | 356 | 357 | 358 | 359 | True 360 | False 361 | 362 | 363 | False 364 | True 365 | 4 366 | 367 | 368 | 369 | 370 | True 371 | False 372 | 18 373 | True 374 | 375 | 376 | True 377 | False 378 | 379 | 380 | True 381 | False 382 | Lower 383 | 384 | 385 | False 386 | True 387 | 0 388 | 389 | 390 | 391 | 392 | True 393 | True 394 | Lower cutoff frequency. 395 | 12,00 396 | 397 | 398 | False 399 | True 400 | end 401 | 1 402 | 403 | 404 | 405 | 406 | 0 407 | 0 408 | 409 | 410 | 411 | 412 | True 413 | False 414 | 415 | 416 | True 417 | False 418 | Higher 419 | 420 | 421 | False 422 | True 423 | 0 424 | 425 | 426 | 427 | 428 | True 429 | True 430 | Higher cutoff frequency. 431 | 12,00 432 | 433 | 434 | False 435 | True 436 | end 437 | 1 438 | 439 | 440 | 441 | 442 | 1 443 | 0 444 | 445 | 446 | 447 | 448 | False 449 | True 450 | 5 451 | 452 | 453 | 454 | 455 | True 456 | False 457 | 458 | 459 | False 460 | True 461 | 6 462 | 463 | 464 | 465 | 466 | True 467 | False 468 | 18 469 | True 470 | 471 | 472 | True 473 | False 474 | start 475 | Cava Output Setup 476 | 477 | 478 | 0 479 | 0 480 | 481 | 482 | 483 | 484 | True 485 | False 486 | 12 487 | 488 | 489 | True 490 | False 491 | Stereo mirrors both channels with low frequencies in center. 492 | Mono averages both channels and outputs left to right lowest to highest frequencies. 493 | 494 | Mono 495 | Stereo 496 | 497 | 498 | 499 | True 500 | True 501 | 0 502 | 503 | 504 | 505 | 506 | True 507 | True 508 | True 509 | Restart CAVA utility for applying changes. 510 | 511 | 512 | False 513 | view-refresh-symbolic 514 | 515 | 516 | 517 | 518 | False 519 | True 520 | end 521 | 1 522 | 523 | 524 | 525 | 526 | 1 527 | 0 528 | 529 | 530 | 531 | 532 | False 533 | True 534 | 7 535 | 536 | 537 | 538 | 539 | True 540 | False 541 | 542 | 543 | False 544 | True 545 | 8 546 | 547 | 548 | 549 | 550 | True 551 | True 552 | never 553 | in 554 | 555 | 556 | True 557 | True 558 | Equalizer setup. First bands is for bass, the last ones for treble. 559 | False 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | True 568 | True 569 | 9 570 | 571 | 572 | 573 | 574 | -------------------------------------------------------------------------------- /cavalcade/gui/colors.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | mouse 8 | 720 9 | 480 10 | dialog 11 | 12 | 13 | True 14 | False 15 | 12 16 | 12 17 | 12 18 | 12 19 | vertical 20 | 12 21 | 22 | 23 | True 24 | False 25 | 6 26 | 27 | 28 | True 29 | True 30 | edit-find-symbolic 31 | False 32 | False 33 | 34 | 35 | True 36 | True 37 | 0 38 | 39 | 40 | 41 | 42 | True 43 | True 44 | True 45 | Remove selected color. 46 | none 47 | 48 | 49 | False 50 | user-trash-symbolic 51 | 52 | 53 | 56 | 57 | 58 | False 59 | True 60 | 1 61 | 62 | 63 | 64 | 65 | False 66 | True 67 | 0 68 | 69 | 70 | 71 | 72 | True 73 | True 74 | in 75 | 76 | 77 | True 78 | True 79 | False 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | True 88 | True 89 | 1 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /cavalcade/gui/playerpage.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 999 7 | 1 8 | 10 9 | 10 | 11 | 1 12 | 0.5 13 | 0.01 14 | 10 15 | 16 | 17 | True 18 | False 19 | vertical 20 | 3 21 | 22 | 23 | True 24 | False 25 | 12 26 | 27 | 28 | True 29 | True 30 | True 31 | 34 | 35 | 36 | False 37 | True 38 | 0 39 | 40 | 41 | 42 | 43 | True 44 | True 45 | seek_adjustment 46 | 0 47 | False 48 | 49 | 50 | True 51 | True 52 | 1 53 | 54 | 55 | 56 | 57 | True 58 | True 59 | False 60 | True 61 | center 62 | center 63 | none 64 | vertical 65 | volume_adjustment 66 | audio-volume-muted-symbolic 67 | audio-volume-high-symbolic 68 | audio-volume-low-symbolic 69 | audio-volume-medium-symbolic 70 | 71 | 72 | True 73 | True 74 | center 75 | center 76 | none 77 | 78 | 79 | 80 | 81 | True 82 | True 83 | center 84 | center 85 | none 86 | 87 | 88 | 89 | 90 | False 91 | True 92 | 2 93 | 94 | 95 | 96 | 97 | False 98 | True 99 | 1 100 | 101 | 102 | 103 | 104 | True 105 | False 106 | 107 | 108 | False 109 | True 110 | 2 111 | 112 | 113 | 114 | 115 | 210 116 | True 117 | False 118 | 6 119 | 6 120 | gtk-missing-image 121 | 122 | 123 | False 124 | True 125 | 3 126 | 127 | 128 | 129 | 130 | True 131 | False 132 | 133 | 134 | False 135 | True 136 | 4 137 | 138 | 139 | 140 | 141 | True 142 | False 143 | 4 144 | 8 145 | 12 146 | 147 | 148 | True 149 | False 150 | 151 | 152 | True 153 | True 154 | False 155 | center 156 | 0 157 | True 158 | False 159 | 160 | 161 | False 162 | view-list-symbolic 163 | 164 | 165 | 166 | 167 | False 168 | True 169 | 0 170 | 171 | 172 | 173 | 174 | True 175 | True 176 | False 177 | Show playback queue. 178 | center 179 | 0.5 180 | True 181 | False 182 | list-radiobutton 183 | 184 | 185 | False 186 | media-playlist-consecutive-symbolic 187 | 188 | 189 | 190 | 191 | False 192 | True 193 | 1 194 | 195 | 196 | 199 | 200 | 201 | False 202 | True 203 | end 204 | 0 205 | 206 | 207 | 208 | 209 | True 210 | True 211 | True 212 | Shuffle player tracks. 213 | 214 | 215 | False 216 | media-playlist-shuffle-symbolic 217 | 218 | 219 | 222 | 223 | 224 | False 225 | True 226 | end 227 | 1 228 | 229 | 230 | 231 | 232 | True 233 | False 234 | 6 235 | 236 | 237 | True 238 | True 239 | True 240 | 243 | 244 | 245 | False 246 | True 247 | 0 248 | 249 | 250 | 251 | 252 | True 253 | True 254 | True 255 | 258 | 259 | 260 | False 261 | True 262 | 1 263 | 264 | 265 | 266 | 267 | False 268 | True 269 | end 270 | 2 271 | 272 | 273 | 274 | 275 | True 276 | True 277 | True 278 | edit-find-symbolic 279 | False 280 | False 281 | 282 | 283 | False 284 | True 285 | 3 286 | 287 | 288 | 289 | 290 | False 291 | True 292 | 5 293 | 294 | 295 | 296 | 297 | True 298 | True 299 | never 300 | in 301 | 302 | 303 | True 304 | True 305 | False 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | True 314 | True 315 | 6 316 | 317 | 318 | 319 | 320 | -------------------------------------------------------------------------------- /cavalcade/gui/settings.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | mouse 5 | dialog 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | stack 15 | 16 | 17 | 18 | 19 | 20 | 21 | open-menu-symbolic 22 | 23 | 24 | 27 | 28 | 29 | start 30 | 31 | 32 | 33 | 34 | 35 | 36 | emblem-system-symbolic 37 | 38 | 39 | 42 | 43 | 44 | end 45 | 46 | 47 | 48 | 49 | 50 | 51 | 6 52 | 500 53 | 6 54 | 12 55 | 12 56 | 12 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /cavalcade/gui/visualpage.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | True 7 | False 8 | vertical 9 | 8 10 | 11 | 12 | True 13 | False 14 | 6 15 | 18 16 | True 17 | 18 | 19 | True 20 | False 21 | 6 22 | 18 23 | True 24 | 25 | 26 | True 27 | False 28 | 12 29 | 30 | 31 | True 32 | False 33 | Offset 34 | 35 | 36 | False 37 | True 38 | 0 39 | 40 | 41 | 42 | 43 | 100 44 | True 45 | True 46 | Offset value. 47 | 48 | 49 | False 50 | True 51 | end 52 | 1 53 | 54 | 55 | 56 | 57 | False 58 | True 59 | 0 60 | 61 | 62 | 63 | 64 | True 65 | False 66 | 67 | 68 | True 69 | True 70 | 1 71 | 72 | 73 | 74 | 75 | 0 76 | 3 77 | 2 78 | 79 | 80 | 81 | 82 | True 83 | False 84 | 12 85 | 86 | 87 | True 88 | False 89 | Silence 90 | 91 | 92 | False 93 | True 94 | 0 95 | 96 | 97 | 98 | 99 | 100 100 | True 101 | True 102 | Number of empty iterations before halting. 103 | 104 | 105 | False 106 | True 107 | end 108 | 1 109 | 110 | 111 | 112 | 113 | 1 114 | 2 115 | 116 | 117 | 118 | 119 | True 120 | False 121 | 12 122 | 123 | 124 | True 125 | False 126 | Padding 127 | 128 | 129 | False 130 | True 131 | 0 132 | 133 | 134 | 135 | 136 | 100 137 | True 138 | True 139 | Bars padding. 140 | 5 141 | 142 | 143 | False 144 | True 145 | end 146 | 1 147 | 148 | 149 | 150 | 151 | 1 152 | 1 153 | 154 | 155 | 156 | 157 | True 158 | False 159 | 12 160 | 161 | 162 | True 163 | False 164 | Zero 165 | 166 | 167 | False 168 | True 169 | 0 170 | 171 | 172 | 173 | 174 | 100 175 | True 176 | True 177 | Draw zero spectrum values. 178 | 179 | 180 | False 181 | True 182 | end 183 | 1 184 | 185 | 186 | 187 | 188 | 0 189 | 2 190 | 191 | 192 | 193 | 194 | True 195 | False 196 | 12 197 | 198 | 199 | True 200 | False 201 | Scale 202 | 203 | 204 | False 205 | True 206 | 0 207 | 208 | 209 | 210 | 211 | 100 212 | True 213 | True 214 | Multiplier for spectrum values. 215 | 1 216 | 1 217 | 218 | 219 | False 220 | True 221 | end 222 | 1 223 | 224 | 225 | 226 | 227 | 0 228 | 1 229 | 230 | 231 | 232 | 233 | True 234 | False 235 | 6 236 | 12 237 | Drawing Geometry 238 | 239 | 240 | 241 | 242 | 243 | 0 244 | 0 245 | 2 246 | 247 | 248 | 249 | 250 | False 251 | True 252 | 1 253 | 254 | 255 | 256 | 257 | True 258 | False 259 | 260 | 261 | False 262 | True 263 | 2 264 | 265 | 266 | 267 | 268 | True 269 | False 270 | vertical 271 | 6 272 | 273 | 274 | True 275 | False 276 | 6 277 | 12 278 | Color Setup 279 | 280 | 281 | 282 | 283 | 284 | False 285 | True 286 | 0 287 | 288 | 289 | 290 | 291 | True 292 | True 293 | Minimum threshold for auto color value. 294 | 6 295 | 2 296 | 2 297 | right 298 | 299 | 300 | False 301 | True 302 | 1 303 | 304 | 305 | 306 | 307 | True 308 | True 309 | Minimum threshold for auto color saturation. 310 | 2 311 | 2 312 | right 313 | 314 | 315 | False 316 | True 317 | 2 318 | 319 | 320 | 321 | 322 | True 323 | False 324 | 6 325 | 18 326 | True 327 | 328 | 329 | True 330 | False 331 | 12 332 | 333 | 334 | True 335 | False 336 | Bands 337 | 338 | 339 | False 340 | True 341 | 0 342 | 343 | 344 | 345 | 346 | 100 347 | True 348 | True 349 | Auto color sampling step. 350 | 351 | 352 | False 353 | True 354 | end 355 | 1 356 | 357 | 358 | 359 | 360 | False 361 | True 362 | 0 363 | 364 | 365 | 366 | 367 | True 368 | False 369 | 12 370 | 371 | 372 | True 373 | False 374 | Window 375 | 376 | 377 | False 378 | True 379 | 0 380 | 381 | 382 | 383 | 384 | 100 385 | True 386 | True 387 | Auto color window width. 388 | 389 | 390 | False 391 | True 392 | end 393 | 1 394 | 395 | 396 | 397 | 398 | False 399 | True 400 | 1 401 | 402 | 403 | 404 | 405 | False 406 | True 407 | 3 408 | 409 | 410 | 411 | 412 | False 413 | True 414 | 3 415 | 416 | 417 | 418 | 419 | True 420 | False 421 | 422 | 423 | False 424 | True 425 | 4 426 | 427 | 428 | 429 | 430 | True 431 | False 432 | 6 433 | 434 | 435 | True 436 | True 437 | True 438 | Save foreground color settings for current audio file. 439 | none 440 | 441 | 442 | False 443 | document-save-symbolic 444 | 445 | 446 | 449 | 450 | 451 | False 452 | True 453 | end 454 | 0 455 | 456 | 457 | 458 | 459 | True 460 | True 461 | True 462 | Update autocolor value. 463 | 464 | 465 | False 466 | view-refresh-symbolic 467 | 468 | 469 | 472 | 473 | 474 | False 475 | True 476 | 1 477 | 478 | 479 | 480 | 481 | True 482 | True 483 | True 484 | Custom drawing color. 485 | True 486 | 487 | 488 | False 489 | True 490 | end 491 | 3 492 | 493 | 494 | 495 | 496 | True 497 | False 498 | Manual Color Setup 499 | 500 | 501 | False 502 | True 503 | 4 504 | 505 | 506 | 507 | 508 | True 509 | True 510 | True 511 | Custom background color. 512 | True 513 | 514 | 515 | False 516 | True 517 | end 518 | 4 519 | 520 | 521 | 522 | 523 | False 524 | True 525 | 5 526 | 527 | 528 | 529 | 530 | True 531 | False 532 | 533 | 534 | False 535 | True 536 | 6 537 | 538 | 539 | 540 | 541 | True 542 | False 543 | 12 544 | 545 | 546 | True 547 | True 548 | True 549 | Select default background image. 550 | 551 | 552 | False 553 | folder-symbolic 554 | 555 | 556 | 559 | 560 | 561 | False 562 | True 563 | 0 564 | 565 | 566 | 567 | 568 | True 569 | False 570 | 571 | 572 | True 573 | True 574 | False 575 | Use default background image. 576 | 0 577 | True 578 | True 579 | False 580 | 581 | 582 | False 583 | starred-symbolic 584 | 585 | 586 | 587 | 588 | False 589 | True 590 | 1 591 | 592 | 593 | 594 | 595 | True 596 | True 597 | False 598 | Use image from audio tag as background image. 599 | 0 600 | True 601 | False 602 | image-file-radiobutton 603 | 604 | 605 | False 606 | audio-x-generic-symbolic 607 | 608 | 609 | 610 | 611 | False 612 | True 613 | 2 614 | 615 | 616 | 619 | 620 | 621 | False 622 | False 623 | end 624 | 2 625 | 626 | 627 | 628 | 629 | True 630 | True 631 | never 632 | in 633 | 634 | 635 | True 636 | False 637 | 638 | 639 | True 640 | False 641 | Image: 642 | 0 643 | 644 | 645 | 646 | 647 | 648 | 649 | True 650 | True 651 | 3 652 | 653 | 654 | 655 | 656 | False 657 | True 658 | 7 659 | 660 | 661 | 662 | 663 | True 664 | False 665 | 666 | 667 | False 668 | True 669 | 8 670 | 671 | 672 | 673 | 674 | -------------------------------------------------------------------------------- /cavalcade/gui/winstate.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | winstate.below 7 | Below 8 | 9 | 10 | winstate.stick 11 | Stick 12 | 13 | 14 | winstate.maximize 15 | Maximize 16 | 17 | 18 | winstate.fullscreen 19 | Fullscreen 20 | 21 | 22 | winstate.skiptaskbar 23 | Skip taskbar 24 | 25 | 26 | winstate.winbyscreen 27 | Resize window to screen 28 | 29 |
30 |
31 | 32 | Color control 33 |
34 | 35 | winstate.bgpaint 36 | Solid background 37 | 38 | 39 | settings.autocolor 40 | Use color analyzer 41 | 42 |
43 |
44 | 45 | Window hint 46 |
47 | 48 | winstate.hint 49 | NORMAL 50 | Normal 51 | 52 | 53 | winstate.hint 54 | DIALOG 55 | Dialog 56 | 57 | 58 | winstate.hint 59 | DOCK 60 | Dock 61 | 62 | 63 | winstate.hint 64 | DESKTOP 65 | Desktop 66 | 67 | 68 | winstate.hint 69 | SPLASHSCREEN 70 | Splashscreen 71 | 72 |
73 |
74 | 75 | 76 | winstate.imagebyscreen 77 | Resize image to screen 78 | 79 | Image alignment 80 |
81 | 82 | winstate.ialign 83 | ; 84 | Top left 85 | 86 | 87 | winstate.ialign 88 | 1; 89 | Top right 90 | 91 | 92 | winstate.ialign 93 | ;1 94 | Bottom left 95 | 96 | 97 | winstate.ialign 98 | 1;1 99 | Bottom right 100 | 101 |
102 |
103 |
104 |
105 | 106 | winstate.image 107 | Show background image 108 | 109 |
110 |
111 |
112 | -------------------------------------------------------------------------------- /cavalcade/logger.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import types 3 | from itertools import chain 4 | import logging 5 | 6 | # The background is set with 40 plus the number of the color, and the foreground with 30 7 | CI = dict(zip(("BLACK", "RED", "GREEN", "YELLOW", "BLUE", "MAGENTA", "CYAN", "WHITE"), range(8))) 8 | COLORS = {'WARNING': CI["YELLOW"], 'INFO': CI["GREEN"], 'DEBUG': CI["BLUE"], 'CRITICAL': CI["RED"], 'ERROR': CI["RED"]} 9 | 10 | RESET_SEQ = "\033[0m" 11 | COLOR_SEQ = "\033[0;%dm" 12 | BOLD_SEQ = "\033[1m" 13 | 14 | COLOR_PACK = list(('$' + color, COLOR_SEQ % (30 + CI[color])) for color in CI.keys()) 15 | 16 | MESSAGE_PATTERN = ( 17 | "$COLOR$BOLD%(levelname)s: $RESET$COLOR%(asctime)s %(filename)s:%(funcName)s():L%(lineno)d " 18 | "$RESET%(message)s" 19 | ) 20 | 21 | FUNCTION_PATTERN = "$COLOR$BOLD%(levelname)s: $RESET$COLOR%(asctime)s $RESET%(message)s" 22 | 23 | _tabbing = 0 24 | _tab = ">>" 25 | 26 | 27 | class ColoredFormatter(logging.Formatter): 28 | """Colored log output formatter""" 29 | def format(self, record): 30 | level_color = COLOR_SEQ % (30 + COLORS[record.levelname]) 31 | message = super().format(record) 32 | for rep in [('$RESET', RESET_SEQ), ('$BOLD', BOLD_SEQ), ('$COLOR', level_color)] + COLOR_PACK: 33 | message = message.replace(*rep) 34 | return message + RESET_SEQ 35 | 36 | 37 | logger = logging.getLogger(__name__) 38 | color_formatter = ColoredFormatter(MESSAGE_PATTERN) 39 | function_formatter = ColoredFormatter(FUNCTION_PATTERN) 40 | 41 | stream_handler = logging.StreamHandler(stream=sys.stdout) 42 | stream_handler.setFormatter(color_formatter) 43 | logger.addHandler(stream_handler) 44 | 45 | 46 | def is_debug(inst): 47 | return inst.getEffectiveLevel() == logging.DEBUG 48 | 49 | 50 | # noinspection PyArgumentList 51 | logger.is_debug = types.MethodType(is_debug, logger) 52 | 53 | 54 | def debuginfo(input_log=True, output_log=True): 55 | """Decorator to log function details. 56 | :param input_log: show function arguments 57 | :param output_log: show function result 58 | :return: function wrapped for logging 59 | """ 60 | def real_decorator(fn): 61 | if logger.getEffectiveLevel() > logging.DEBUG: 62 | return fn 63 | 64 | def wrapped(*args, **kwargs): 65 | global _tabbing 66 | name = fn.__qualname__.split('.')[-1] 67 | filename = fn.__code__.co_filename.split('/')[-1] 68 | lineno = fn.__code__.co_firstlineno 69 | params = ", ".join(map(repr, chain(args, kwargs.values()))) 70 | 71 | # print function name 72 | stream_handler.setFormatter(function_formatter) 73 | logger.debug( 74 | "$BOLD$CYAN%s%s %s:$MAGENTA%s$CYAN:L%s", 75 | _tab * _tabbing, "FUNCTION", filename, name, lineno 76 | ) 77 | 78 | # print function arguments 79 | if input_log: 80 | logger.debug( 81 | "$BOLD$CYAN%s%s $MAGENTA%s$CYAN: $RESET%s", 82 | _tab * _tabbing, "INPUT", name, params 83 | ) 84 | stream_handler.setFormatter(color_formatter) 85 | 86 | # run original function 87 | _tabbing += 1 88 | returned_value = fn(*args, **kwargs) 89 | _tabbing -= 1 90 | 91 | # print function result 92 | stream_handler.setFormatter(function_formatter) 93 | if output_log: 94 | logger.debug( 95 | "$BOLD$CYAN%s%s $MAGENTA%s$CYAN: $RESET%s", 96 | _tab * _tabbing, "OUTPUT", name, repr(returned_value) 97 | ) 98 | logger.debug("$BOLD$CYAN%s%s $MAGENTA%s", _tab * _tabbing, "FINISHED", name) 99 | stream_handler.setFormatter(color_formatter) 100 | 101 | return returned_value 102 | 103 | return wrapped 104 | return real_decorator 105 | -------------------------------------------------------------------------------- /cavalcade/mainapp.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | import signal 3 | from gi.repository import Gtk, GObject, Gio, GLib 4 | 5 | from cavalcade.config import MainConfig, CavaConfig 6 | from cavalcade.drawing import Spectrum 7 | from cavalcade.cava import Cava 8 | from cavalcade.settings import SettingsWindow 9 | from cavalcade.logger import logger 10 | from cavalcade.canvas import Canvas 11 | from cavalcade.adata import AudioData, SavedColors 12 | from cavalcade.common import set_actions, import_optional 13 | from cavalcade.version import get_current as get_current_version 14 | 15 | 16 | class MainApp(Gtk.Application): 17 | """Main application class""" 18 | __gsignals__ = { 19 | "tag-image-update": (GObject.SIGNAL_RUN_FIRST, None, (object,)), 20 | "default-image-update": (GObject.SIGNAL_RUN_FIRST, None, (str,)), 21 | "image-source-switch": (GObject.SIGNAL_RUN_FIRST, None, (bool,)), 22 | "autocolor-refresh": (GObject.SIGNAL_RUN_FIRST, None, (bool,)), 23 | "ac-update": (GObject.SIGNAL_RUN_FIRST, None, (object,)), 24 | } 25 | 26 | def __init__(self): 27 | super().__init__(application_id="com.github.worron.cavalcade", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE) 28 | 29 | signal.signal(signal.SIGINT, self.gracefully_close) 30 | signal.signal(signal.SIGTERM, self.gracefully_close) 31 | 32 | self.add_main_option( 33 | "play", ord("p"), GLib.OptionFlags.NONE, GLib.OptionArg.NONE, 34 | "Start audio playing on launch", None 35 | ) 36 | self.add_main_option( 37 | "version", ord("v"), GLib.OptionFlags.NONE, GLib.OptionArg.NONE, 38 | "Show application version", None 39 | ) 40 | self.add_main_option( 41 | "restore", ord("r"), GLib.OptionFlags.NONE, GLib.OptionArg.NONE, 42 | "Restore previous player session", None 43 | ) 44 | self.add_main_option( 45 | "quit", ord("q"), GLib.OptionFlags.NONE, GLib.OptionArg.NONE, 46 | "Exit program", None 47 | ) 48 | # this is fake one, real log level set on script launch (see run.py) 49 | self.add_main_option( 50 | "log-level", ord("l"), GLib.OptionFlags.NONE, GLib.OptionArg.STRING, 51 | "Set log level", "LOG_LEVEL" 52 | ) 53 | 54 | self.connect("handle-local-options", self._on_handle_local_options) 55 | 56 | def do_activate(self): 57 | if not hasattr(self, "canvas"): 58 | self._do_startup() 59 | 60 | self.canvas.window.present() 61 | 62 | def do_command_line(self, command_line): 63 | args = command_line.get_arguments()[1:] 64 | options = command_line.get_options_dict() 65 | 66 | # show version and exit 67 | if options.contains("version"): 68 | return 0 69 | 70 | # main app launch 71 | self.activate() 72 | self._parse_args(args, options) 73 | 74 | # some special handlers on startup 75 | if not options.contains("play") and self.imported.pillow and self.config["color"]["auto"]: 76 | self.autocolor.color_update(self.config["image"]["default"]) 77 | 78 | return 0 79 | 80 | def do_shutdown(self): 81 | if hasattr(self, "canvas"): 82 | self.cava.close() 83 | self.adata.save() 84 | self.palette.save() 85 | 86 | if not self.config.is_fallback: 87 | self.config.write_data() 88 | else: 89 | logger.warning("User config is not available, all settings changes will be lost") 90 | 91 | logger.info("Exit cavalcade") 92 | Gtk.Application.do_shutdown(self) 93 | 94 | def _do_startup(self): 95 | """Main initialization function""" 96 | # check modules 97 | logger.info("Start cavalcade") 98 | self.imported = import_optional() 99 | 100 | # load config 101 | self.config = MainConfig() 102 | self.cavaconfig = CavaConfig() 103 | 104 | # init app structure 105 | self.adata = AudioData(self) # audio files manager 106 | self.palette = SavedColors(self) # custom colors list 107 | self.draw = Spectrum(self.config, self.cavaconfig) # graph widget 108 | self.cava = Cava(self) # cava wrapper 109 | self.settings = SettingsWindow(self) # settings window 110 | self.canvas = Canvas(self) # main window 111 | 112 | # optional image analyzer 113 | if self.imported.pillow: 114 | from cavalcade.autocolor import AutoColor 115 | 116 | self.autocolor = AutoColor(self) 117 | else: 118 | logger.info("Starting without auto color detection function") 119 | 120 | # optional gstreamer player 121 | if self.imported.gstreamer: 122 | from cavalcade.player import Player 123 | 124 | self.player = Player(self) 125 | self.settings.add_player_page() 126 | self.canvas.actions.update(self.player.actions) 127 | else: 128 | logger.info("Starting without audio player function") 129 | 130 | # set actions 131 | quit_action = Gio.SimpleAction.new("quit", None) 132 | quit_action.connect("activate", self.close) 133 | self.add_action(quit_action) 134 | 135 | # share actions 136 | self.canvas.actions.update(self.settings.actions) 137 | set_actions(self.canvas.actions, self.settings.gui["window"]) 138 | 139 | # accelerators 140 | self.add_accelerator(self.config["keys"]["play"], "player.play", None) 141 | self.add_accelerator(self.config["keys"]["next"], "player.next", None) 142 | self.add_accelerator(self.config["keys"]["exit"], "app.quit", None) 143 | self.add_accelerator(self.config["keys"]["show"], "settings.show", None) 144 | self.add_accelerator(self.config["keys"]["hide"], "settings.hide", None) 145 | 146 | # start work 147 | self.canvas.setup() 148 | self.cava.start() 149 | 150 | # noinspection PyMethodMayBeStatic 151 | def _on_handle_local_options(self, _, options): 152 | """GUI handler""" 153 | if options.contains("version"): 154 | print(get_current_version()) 155 | return -1 156 | 157 | def _parse_args(self, args, options): 158 | """Parse command line arguments""" 159 | self.adata.load(args) 160 | if options.contains("restore"): 161 | self.adata.restore() 162 | self.adata.send_to_player() 163 | 164 | if options.contains("play"): 165 | self.canvas.run_action("player", "play") 166 | 167 | if options.contains("quit"): 168 | self.close() 169 | 170 | # noinspection PyUnusedLocal 171 | def gracefully_close(self, signum, frame): 172 | """ 173 | Termination signals handler. 174 | Nothing else but regular exit actually. 175 | """ 176 | logger.info("Exit on %d signal" % signum) 177 | self.quit() 178 | 179 | # noinspection PyUnusedLocal 180 | def close(self, *args): 181 | """Application exit""" 182 | self.quit() 183 | -------------------------------------------------------------------------------- /cavalcade/pixbuf.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | from gi.repository import Gio, GLib, GdkPixbuf 3 | 4 | 5 | def from_bytes(data): 6 | """Build Gdk pixbuf from bytedata""" 7 | stream = Gio.MemoryInputStream.new_from_bytes(GLib.Bytes.new(data)) 8 | pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream) 9 | return pixbuf 10 | 11 | 12 | def from_bytes_at_scale(data, width, height, aspect=True): 13 | """Build Gdk pixbuf from bytedata with scaling""" 14 | stream = Gio.MemoryInputStream.new_from_bytes(GLib.Bytes.new(data)) 15 | pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(stream, width, height, aspect) 16 | return pixbuf 17 | 18 | 19 | def from_file_at_scale(file_, width, height, aspect=True): 20 | """Build Gdk pixbuf from file with scaling""" 21 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(file_, width, height, aspect) 22 | return pixbuf 23 | -------------------------------------------------------------------------------- /cavalcade/player.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | import gi 3 | import random 4 | gi.require_version('Gst', '1.0') 5 | 6 | from gi.repository import Gst, GLib, GObject, Gio 7 | from cavalcade.logger import logger 8 | 9 | Gst.init(None) 10 | 11 | 12 | # TODO: separate thread? 13 | def image_data_from_message(message): 14 | """Get image bytedata from id3 tag message""" 15 | taglist = message.parse_tag() 16 | is_ok, sample = taglist.get_sample("image") 17 | if not is_ok: 18 | return None 19 | 20 | gstreamer_buffer = sample.get_buffer() 21 | map_info = gstreamer_buffer.map(Gst.MapFlags.READ)[1] 22 | data = map_info.data 23 | gstreamer_buffer.unmap(map_info) 24 | return data 25 | 26 | 27 | class Player(GObject.GObject): 28 | """Simple gstreamer audio player""" 29 | __gsignals__ = { 30 | "progress": (GObject.SIGNAL_RUN_FIRST, None, (int,)), 31 | "playlist-update": (GObject.SIGNAL_RUN_FIRST, None, (object,)), 32 | "queue-update": (GObject.SIGNAL_RUN_FIRST, None, (object,)), 33 | "current": (GObject.SIGNAL_RUN_FIRST, None, (object,)), 34 | "preview-update": (GObject.SIGNAL_RUN_FIRST, None, (object,)), 35 | "playing": (GObject.SIGNAL_RUN_FIRST, None, (bool,)), 36 | } 37 | 38 | def __init__(self, mainapp): 39 | super().__init__() 40 | self._mainapp = mainapp 41 | self.config = mainapp.config 42 | self.playlist = [] 43 | self.playqueue = [] 44 | self.actions = {} 45 | 46 | self.is_image_updated = True 47 | self.duration = None 48 | self.timer_id = None 49 | self._current = None 50 | self._is_playing = False 51 | 52 | self.player = Gst.ElementFactory.make('playbin', 'player') 53 | 54 | bus = self.player.get_bus() 55 | bus.add_signal_watch() 56 | bus.connect("message", self._on_message) 57 | bus.connect("message::tag", self._on_message_tag) 58 | 59 | # this is ugly and should be fixed 60 | # some fake player object is used to get tag image without playing audio file itself 61 | self._fake_player = Gst.ElementFactory.make('playbin', 'player') 62 | bus = self._fake_player.get_bus() 63 | bus.add_signal_watch() 64 | bus.connect("message::tag", self._on_fake_message) 65 | 66 | # actions 67 | self.actions["player"] = Gio.SimpleActionGroup() 68 | 69 | play_action = Gio.SimpleAction.new("play", None) 70 | play_action.connect("activate", self.play_pause) 71 | self.actions["player"].add_action(play_action) 72 | 73 | next_action = Gio.SimpleAction.new("next", None) 74 | next_action.connect("activate", self.play_next) 75 | self.actions["player"].add_action(next_action) 76 | 77 | @property 78 | def current(self): 79 | return self._current 80 | 81 | @current.setter 82 | def current(self, value): 83 | self._current = value 84 | self.emit("current", value) 85 | 86 | @property 87 | def is_playing(self): 88 | return self._is_playing 89 | 90 | @is_playing.setter 91 | def is_playing(self, value): 92 | if self._is_playing != value: 93 | self._is_playing = value 94 | if value: 95 | self.timer_id = GLib.timeout_add(1000, self._progress) 96 | else: 97 | GLib.source_remove(self.timer_id) 98 | self.emit("playing", value) 99 | 100 | def _progress(self): 101 | if self.duration is None: 102 | success, self.duration = self.player.query_duration(Gst.Format.TIME) 103 | if not success: 104 | logger.warning("Couldn't fetch song duration") 105 | self.duration = None 106 | return True 107 | success, position = self.player.query_position(Gst.Format.TIME) 108 | if success: 109 | self.emit("progress", (position / self.duration * 1000)) 110 | else: 111 | logger.warning("Couldn't fetch current song position to update slider") 112 | return True 113 | 114 | # noinspection PyUnusedLocal 115 | def _on_message(self, bus, message): 116 | if message.type == Gst.MessageType.EOS: 117 | self.play_next() # this one should do all clear 118 | elif message.type == Gst.MessageType.ERROR: 119 | self.stop() 120 | err, debug = message.parse_error() 121 | logger.error("Playback error %s\n%s" % (err, debug)) 122 | 123 | # noinspection PyUnusedLocal 124 | def _on_message_tag(self, bus, message): 125 | if not self.is_image_updated: 126 | self.is_image_updated = True 127 | data = image_data_from_message(message) 128 | self._mainapp.emit("tag-image-update", data) 129 | 130 | def load_playlist(self, files, queue=None): 131 | """ 132 | Set list of audio files for player. 133 | Playback queue may be settled as optional argument. 134 | """ 135 | self.stop() 136 | 137 | if files: 138 | self.playlist = files 139 | 140 | self.playqueue = list(queue if queue else files) 141 | self.emit("playlist-update", self.playlist) 142 | self.emit("queue-update", self.playqueue) 143 | self.load_file(random.choice(self.playqueue) if self.config["player"]["shuffle"] else self.playqueue[0]) 144 | 145 | def load_file(self, file_): 146 | """Set audio file to play""" 147 | if self.current is not None: 148 | if self.current in self.playqueue: 149 | self.playqueue.remove(self.current) 150 | self.stop() 151 | 152 | self.is_image_updated = False 153 | self.current = file_ 154 | self.player.set_property('uri', 'file:///' + file_) 155 | if file_ not in self.playqueue: 156 | self.playqueue.append(file_) 157 | self.emit("queue-update", self.playqueue) 158 | 159 | def add_to_queue(self, *files): 160 | """Add audio file to playback queue""" 161 | updated = False 162 | for file_ in files: 163 | if file_ not in self.playqueue: 164 | self.playqueue.append(file_) 165 | updated = True 166 | 167 | if updated: 168 | self.emit("queue-update", self.playqueue) 169 | 170 | def remove_from_queue(self, *files): 171 | """Remove audio file from playback queue""" 172 | updated = False 173 | for file_ in files: 174 | if file_ in self.playqueue: 175 | self.playqueue.remove(file_) 176 | updated = True 177 | 178 | if updated: 179 | self.emit("queue-update", self.playqueue) 180 | 181 | def seek(self, value): 182 | """Playback progress manipulation""" 183 | if self.duration is not None: 184 | point = int(self.duration * value / 1000) 185 | self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, point) 186 | 187 | def stop(self): 188 | """Stop playback""" 189 | self.player.set_state(Gst.State.NULL) 190 | self.is_playing = False 191 | self.duration = None 192 | self.current = None 193 | 194 | # noinspection PyUnusedLocal 195 | def play_next(self, *args): 196 | """Play next audio file in queue""" 197 | current = self.current 198 | self.stop() 199 | 200 | if current is None: 201 | logger.debug("No audio file selected") 202 | else: 203 | if current in self.playqueue: 204 | i = self.playqueue.index(current) 205 | self.playqueue.remove(current) 206 | else: 207 | i = 1 208 | if self.playqueue: 209 | if self.config["player"]["shuffle"]: 210 | self.load_file(random.choice(self.playqueue)) 211 | elif i < len(self.playqueue): 212 | self.load_file(self.playqueue[i]) 213 | else: 214 | self.load_file(self.playqueue[0]) 215 | self.play_pause() 216 | self.emit("queue-update", self.playqueue) # fix false update if current not in queue 217 | 218 | # noinspection PyUnusedLocal 219 | def play_pause(self, *args): 220 | """Play or pause""" 221 | if self.current is None: 222 | logger.debug("No audio file selected") 223 | return None 224 | 225 | if not self.is_playing: 226 | self.player.set_state(Gst.State.PLAYING) 227 | self.is_playing = True 228 | else: 229 | self.player.set_state(Gst.State.PAUSED) 230 | self.is_playing = False 231 | 232 | def set_volume(self, value): 233 | """Volume manipulation""" 234 | self.player.set_property('volume', value) 235 | 236 | def fake_tag_reader(self, file_): 237 | self._fake_player.set_property('uri', 'file:///' + file_) 238 | self._fake_player.set_state(Gst.State.PAUSED) 239 | 240 | # noinspection PyUnusedLocal 241 | def _on_fake_message(self, bus, message): 242 | data = image_data_from_message(message) 243 | self.emit("preview-update", data) 244 | self._fake_player.set_state(Gst.State.NULL) 245 | -------------------------------------------------------------------------------- /cavalcade/playerpage.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | import cavalcade.pixbuf as pixbuf 3 | 4 | from gi.repository import Gtk, Pango 5 | from cavalcade.common import GuiBase, TreeViewHolder, name_from_file, AttributeDict 6 | 7 | 8 | class PlayerPage(GuiBase): 9 | """Player setting page""" 10 | def __init__(self, mainapp): 11 | self._mainapp = mainapp 12 | self.preview = None 13 | self.current = None 14 | self.playlist = [] 15 | self.playqueue = [] 16 | 17 | elements = ( 18 | "mainbox", "play-button", "seek-scale", "playlist-treeview", "playlist-selection", "preview-image", 19 | "volumebutton", "list-searchentry", "queue-radiobutton", "list-radiobutton", "solo-action-button", 20 | "mass-action-button", "shuffle-button", 21 | ) 22 | super().__init__("playerpage.glade", elements=elements) 23 | 24 | # some gui constants 25 | self.TRACK_STORE = AttributeDict(INDEX=0, NAME=1, FILE=2) 26 | self.PLAY_BUTTON_DATA = { 27 | False: Gtk.Image(icon_name="media-playback-start-symbolic"), 28 | True: Gtk.Image(icon_name="media-playback-pause-symbolic") 29 | } 30 | self.ACTION_BUTTON_DATA = dict( 31 | list = AttributeDict( 32 | images = (Gtk.Image(icon_name="list-add-symbolic"), Gtk.Image(icon_name="send-to-symbolic")), 33 | tooltip = ("Add track to playback queue.", "Add all to playback queue.") 34 | ), 35 | queue = AttributeDict( 36 | images = (Gtk.Image(icon_name="list-remove-symbolic"), Gtk.Image(icon_name="list-remove-all-symbolic")), 37 | tooltip = ("Remove track from playback queue.", "Clear playback queue.") 38 | ) 39 | ) 40 | 41 | # get preview widget height 42 | pz = self.gui["preview-image"].get_preferred_size()[1] 43 | self.preview_size = pz.height - 2 44 | self.update_default_preview() 45 | 46 | # playlist view setup 47 | self.treeview = self.gui["playlist-treeview"] 48 | self.treelock = TreeViewHolder(self.treeview) 49 | for i, title in enumerate(("Index", "Name", "File")): 50 | column = Gtk.TreeViewColumn(title, Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END), text=i) 51 | self.treeview.append_column(column) 52 | if i != self.TRACK_STORE.NAME: 53 | column.set_visible(False) 54 | 55 | self.store = Gtk.ListStore(int, str, str) 56 | self.store_filter = self.store.filter_new() 57 | self.store_filter.set_visible_func(self.playlist_filter_func) 58 | self.search_text = None 59 | 60 | self.treeview.set_model(self.store_filter) 61 | 62 | # list view button 63 | list_radio_button = "queue-radiobutton" if self._mainapp.config["player"]["showqueue"] else "list-radiobutton" 64 | self.gui[list_radio_button].set_active(True) 65 | 66 | self.gui["queue-radiobutton"].connect("notify::active", self.on_listview_radio_button_switch, True) 67 | self.gui["list-radiobutton"].connect("notify::active", self.on_listview_radio_button_switch, False) 68 | 69 | # list action buttons 70 | self.set_button_images() 71 | self.gui["play-button"].set_image(self.PLAY_BUTTON_DATA[False]) 72 | 73 | # signals 74 | self.gui["play-button"].connect("clicked", self.on_playbutton_click) 75 | self.gui["mass-action-button"].connect("clicked", self.on_mass_button_click) 76 | self.gui["solo-action-button"].connect("clicked", self.on_solo_button_click) 77 | self.gui["playlist-treeview"].connect("row_activated", self.on_track_activated) 78 | self.gui["volumebutton"].connect("value-changed", self.on_volumebuton_changed) 79 | self.gui["list-searchentry"].connect("activate", self.on_search_active) 80 | self.gui["list-searchentry"].connect("icon-release", self.on_search_reset) 81 | self.gui["shuffle-button"].connect("toggled", self.on_shuffle_button_toggle) 82 | self.seek_handler_id = self.gui["seek-scale"].connect("value-changed", self.on_seekscale_changed) 83 | self.sel_handler_id = self.gui["playlist-selection"].connect("changed", self.on_track_selection_changed) 84 | 85 | self._mainapp.player.connect("progress", self.on_audio_progress) 86 | self._mainapp.player.connect("playlist-update", self.on_playlist_update) 87 | self._mainapp.player.connect("queue-update", self.on_playqueue_update) 88 | self._mainapp.player.connect("current", self.on_current_change) 89 | self._mainapp.player.connect("preview-update", self.on_preview_update) 90 | self._mainapp.player.connect("playing", self.on_play_state_update) 91 | 92 | self._mainapp.connect("default-image-update", self.update_default_preview) 93 | 94 | # gui setup 95 | self.gui["volumebutton"].set_value(self._mainapp.config["player"]["volume"]) 96 | self.gui["shuffle-button"].set_active(self._mainapp.config["player"]["shuffle"]) 97 | 98 | # support 99 | def on_shuffle_button_toggle(self, button): 100 | self._mainapp.config["player"]["shuffle"] = button.get_active() 101 | 102 | # noinspection PyUnusedLocal 103 | def update_default_preview(self, *args): 104 | self.preview = pixbuf.from_file_at_scale(self._mainapp.config["image"]["default"], -1, self.preview_size) 105 | 106 | def set_button_images(self): 107 | """Update action buttons images according current state""" 108 | data = self.ACTION_BUTTON_DATA["queue" if self._mainapp.config["player"]["showqueue"] else "list"] 109 | for i, button_name in enumerate(("solo-action-button", "mass-action-button")): 110 | self.gui[button_name].set_image(data.images[i]) 111 | self.gui[button_name].set_tooltip_text(data.tooltip[i]) 112 | 113 | # noinspection PyUnusedLocal 114 | def playlist_filter_func(self, model, treeiter, data): 115 | """Function to filter current track list by search text""" 116 | if not self.search_text: 117 | return True 118 | else: 119 | return self.search_text.lower() in model[treeiter][self.TRACK_STORE.NAME].lower() 120 | 121 | def get_filtered_files(self): 122 | """Get list of files considering search filter""" 123 | return [row[self.TRACK_STORE.FILE] for row in self.store_filter] 124 | 125 | def highlight_current(self): 126 | """Select current playing track if available""" 127 | files = self.get_filtered_files() 128 | if self.current in files: 129 | index = files.index(self.current) 130 | self.treeview.set_cursor(index) 131 | else: 132 | self.gui["playlist-selection"].unselect_all() 133 | 134 | def filter_by_search(self): 135 | """Filter current track list by search text""" 136 | self.search_text = self.gui["list-searchentry"].get_text() 137 | with self.gui["playlist-selection"].handler_block(self.sel_handler_id): 138 | self.store_filter.refilter() 139 | self.gui["playlist-selection"].unselect_all() 140 | 141 | def rebuild_store(self, data): 142 | """Update audio track store""" 143 | with self.treelock: 144 | self.store.clear() 145 | for i, file_ in enumerate(data): 146 | self.store.append([i, name_from_file(file_), file_]) 147 | self.highlight_current() 148 | 149 | # gui handlers 150 | # noinspection PyUnusedLocal,PyUnusedLocal 151 | def on_track_activated(self, tree, path, column): 152 | treeiter = self.store_filter.get_iter(path) 153 | file_ = self.store_filter[treeiter][self.TRACK_STORE.FILE] 154 | self._mainapp.player.load_file(file_) 155 | self._mainapp.player.play_pause() 156 | 157 | # noinspection PyUnusedLocal 158 | def on_playlist_update(self, player, plist): 159 | self.playlist = plist 160 | if not self._mainapp.config["player"]["showqueue"]: 161 | self.rebuild_store(plist) 162 | 163 | # noinspection PyUnusedLocal 164 | def on_playqueue_update(self, player, play_queue): 165 | self.playqueue = play_queue 166 | if self._mainapp.config["player"]["showqueue"]: 167 | self.rebuild_store(play_queue) 168 | 169 | # noinspection PyUnusedLocal 170 | def on_playbutton_click(self, button): 171 | self._mainapp.player.play_pause() 172 | 173 | # noinspection PyUnusedLocal 174 | def on_seekscale_changed(self, widget): 175 | value = self.gui["seek-scale"].get_value() 176 | self._mainapp.player.seek(value) 177 | 178 | # noinspection PyUnusedLocal 179 | def on_volumebuton_changed(self, widget, value): 180 | self._mainapp.config["player"]["volume"] = value 181 | self._mainapp.player.set_volume(value) 182 | 183 | # noinspection PyUnusedLocal 184 | def on_audio_progress(self, player, value): 185 | with self.gui["seek-scale"].handler_block(self.seek_handler_id): 186 | self.gui["seek-scale"].set_value(value) 187 | 188 | # noinspection PyUnusedLocal 189 | def on_current_change(self, player, current): 190 | self.current = current 191 | if current is not None: 192 | self.gui["play-button"].set_tooltip_text("Playing: %s" % name_from_file(current)) 193 | self.highlight_current() 194 | else: 195 | self.gui["play-button"].set_tooltip_text("Playing: none") 196 | 197 | def on_track_selection_changed(self, selection): 198 | model, sel = selection.get_selected() 199 | if sel is not None: 200 | file_ = model[sel][self.TRACK_STORE.FILE] 201 | self._mainapp.player.fake_tag_reader(file_) 202 | 203 | # noinspection PyUnusedLocal 204 | def on_preview_update(self, player, bytedata): 205 | pb = pixbuf.from_bytes_at_scale(bytedata, -1, self.preview_size) if bytedata is not None else self.preview 206 | self.gui["preview-image"].set_from_pixbuf(pb) 207 | 208 | # noinspection PyUnusedLocal 209 | def on_search_active(self, *args): 210 | self.filter_by_search() 211 | self.highlight_current() 212 | 213 | # noinspection PyUnusedLocal 214 | def on_search_reset(self, *args): 215 | self.gui["list-searchentry"].set_text("") 216 | self.on_search_active() 217 | 218 | # noinspection PyUnusedLocal 219 | def on_listview_radio_button_switch(self, button, active, showqueue): 220 | if button.get_active(): 221 | self._mainapp.config["player"]["showqueue"] = showqueue 222 | self.gui["list-searchentry"].set_text("") 223 | self.filter_by_search() 224 | data = self.playqueue if showqueue else self.playlist 225 | self.rebuild_store(data) 226 | self.set_button_images() 227 | 228 | # noinspection PyUnusedLocal 229 | def on_solo_button_click(self, *args): 230 | model, sel = self.gui["playlist-selection"].get_selected() 231 | if sel is not None: 232 | file_ = model[sel][self.TRACK_STORE.FILE] 233 | if self._mainapp.config["player"]["showqueue"]: 234 | self._mainapp.player.remove_from_queue(file_) 235 | else: 236 | self._mainapp.player.add_to_queue(file_) 237 | 238 | # noinspection PyUnusedLocal 239 | def on_mass_button_click(self, *args): 240 | files = self.get_filtered_files() 241 | if self._mainapp.config["player"]["showqueue"]: 242 | self._mainapp.player.remove_from_queue(*files) 243 | else: 244 | self._mainapp.player.add_to_queue(*files) 245 | 246 | # noinspection PyUnusedLocal 247 | def on_play_state_update(self, player, value): 248 | self.gui["play-button"].set_image(self.PLAY_BUTTON_DATA[value]) 249 | -------------------------------------------------------------------------------- /cavalcade/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 3 | 4 | import os 5 | import re 6 | import sys 7 | import gi 8 | import signal 9 | 10 | gi.require_version('Gtk', '3.0') 11 | if __name__ == "__main__": 12 | sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) 13 | 14 | signal.signal(signal.SIGINT, signal.SIG_DFL) 15 | 16 | 17 | def set_log_level(args): 18 | # noinspection PyPep8 19 | from cavalcade.logger import logger 20 | 21 | level = re.search("log-level=(\w+)", str(args)) 22 | try: 23 | logger.setLevel(level.group(1)) 24 | except Exception: 25 | logger.setLevel("WARNING") 26 | 27 | 28 | def run(): 29 | set_log_level(sys.argv) 30 | 31 | # noinspection PyPep8 32 | from cavalcade.mainapp import MainApp 33 | 34 | app = MainApp() 35 | exit_status = app.run(sys.argv) 36 | sys.exit(exit_status) 37 | 38 | 39 | if __name__ == "__main__": 40 | run() 41 | -------------------------------------------------------------------------------- /cavalcade/settings.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | from gi.repository import Gio 3 | from cavalcade.visualpage import VisualPage 4 | from cavalcade.cavapage import CavaPage 5 | from cavalcade.playerpage import PlayerPage 6 | from cavalcade.colordata import ColorsWindow 7 | from cavalcade.common import GuiBase 8 | 9 | 10 | class SettingsWindow(GuiBase): 11 | """Settings window""" 12 | def __init__(self, mainapp): 13 | elements = ( 14 | "window", "headerbar", "winstate-menubutton", "stackswitcher", "app-menu", "stack", "winstate-menu", 15 | "app-menubutton", 16 | ) 17 | super().__init__("settings.ui", "appmenu.ui", "winstate.ui", elements=elements) 18 | 19 | self.actions = {} 20 | self._mainapp = mainapp 21 | self.gui["window"].set_keep_above(True) 22 | self.gui["window"].set_application(mainapp) 23 | self.actions["settings"] = Gio.SimpleActionGroup() 24 | 25 | # add visual page 26 | self.visualpage = VisualPage(self._mainapp, self) 27 | self.gui["stack"].add_titled(self.visualpage.gui["mainbox"], "visset", "Visual") 28 | 29 | # add cava page 30 | self.cavapage = CavaPage(self._mainapp) 31 | self.gui["stack"].add_titled(self.cavapage.gui["mainbox"], "cavaset", "CAVA") 32 | 33 | # add colors dialog 34 | self.colors = ColorsWindow(self._mainapp) 35 | self.colors.gui["window"].set_transient_for(self.gui["window"]) 36 | 37 | # setup menu buttons 38 | self.gui["winstate-menubutton"].set_menu_model(self.gui["winstate-menu"]) 39 | self.gui["app-menubutton"].set_menu_model(self.gui["app-menu"]) 40 | 41 | # actions 42 | hide_action = Gio.SimpleAction.new("hide", None) 43 | hide_action.connect("activate", self.hide) 44 | self.actions["settings"].add_action(hide_action) 45 | 46 | show_action = Gio.SimpleAction.new("show", None) 47 | show_action.connect("activate", self.show) 48 | self.actions["settings"].add_action(show_action) 49 | 50 | colors_action = Gio.SimpleAction.new("colors", None) 51 | colors_action.connect("activate", self.colors.show) 52 | self.actions["settings"].add_action(colors_action) 53 | 54 | # signals 55 | self.gui["window"].connect("delete-event", self.hide) 56 | 57 | def add_player_page(self): 58 | """Optional player page""" 59 | # noinspection PyAttributeOutsideInit 60 | self.playerpage = PlayerPage(self._mainapp) 61 | self.gui["stack"].add_titled(self.playerpage.gui["mainbox"], "playset", "Player") 62 | 63 | # noinspection PyUnusedLocal 64 | def show(self, *args): 65 | """Show settings window""" 66 | self.gui["window"].show_all() 67 | 68 | # noinspection PyUnusedLocal 69 | def hide(self, *args): 70 | """Hide settings window""" 71 | self.gui["window"].hide() 72 | return True 73 | -------------------------------------------------------------------------------- /cavalcade/version.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | _FALLBACK_VERSION = "0.8" 5 | # _DEVELOPMENT_BRANCH = "devel" 6 | _MASTER_BRANCH = "master" 7 | 8 | 9 | def get_current(): 10 | """Try to find current version of package using git output""" 11 | version = _FALLBACK_VERSION 12 | try: 13 | cwd_ = os.path.dirname(os.path.abspath(__file__)) 14 | 15 | output = subprocess.check_output(["git", "describe", "--tags", "--long"], stderr=subprocess.PIPE, cwd=cwd_) 16 | describe = str(output, "utf-8").strip() 17 | output = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], stderr=subprocess.PIPE, cwd=cwd_) 18 | branch = str(output, "utf-8").strip() 19 | 20 | v, n, commit = describe.split('-') 21 | 22 | if branch == _MASTER_BRANCH or n == "0": 23 | # TODO: does it possible to proper count commit on master branch? 24 | version = v 25 | else: 26 | # Just assuming we are working on next version 27 | # if git branch is different from master 28 | next_version = float(v) + 0.1 29 | version = "%.1f.dev%s+%s" % (next_version, n, commit) 30 | except Exception as e: 31 | # use plain print instead of logger to avoid potential error on setup 32 | print("Can't read git output:\n%s", e) 33 | 34 | return version 35 | -------------------------------------------------------------------------------- /cavalcade/visualpage.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 2 | from cavalcade.common import GuiBase, name_from_file, gtk_open_file 3 | from cavalcade.logger import logger 4 | from gi.repository import Gtk, GLib, Gio 5 | 6 | 7 | class VisualPage(GuiBase): 8 | """Visual setting page""" 9 | def __init__(self, mainapp, settings): 10 | self._mainapp = mainapp 11 | self._settings = settings 12 | self.window = settings.gui["window"] 13 | self.config = self._mainapp.config 14 | 15 | elements = ( 16 | "fg-colorbutton", "zero-spinbutton", "silence-spinbutton", "image-open-button", 17 | "bg-colorbutton", "padding-spinbutton", "scale-spinbutton", "image-tag-radiobutton", "image-label", 18 | "image-file-radiobutton", "mainbox", "offset-comboboxtext", "offset-spinbutton", "value-min-scale", 19 | "saturation-min-scale", "ac-window-spinbutton", "ac-bands-spinbutton", "refresh-autocolor-button", 20 | "save-color-button", 21 | ) 22 | super().__init__("visualpage.glade", elements=elements) 23 | 24 | # image file filter 25 | self.image_filter = Gtk.FileFilter() 26 | self.image_filter.set_name("Image files") 27 | self.image_filter.add_pixbuf_formats() 28 | 29 | # color 30 | for key in ("fg", "bg"): 31 | self.gui["%s-colorbutton" % key].set_rgba(self.config["color"][key]) 32 | self.gui["%s-colorbutton" % key].connect("color-set", getattr(self, "on_%s_color_set" % key)) 33 | 34 | # graph 35 | self.gui["scale-spinbutton"].set_adjustment(Gtk.Adjustment(1, 0.5, 5, 0.1, 0, 0)) 36 | self.gui["zero-spinbutton"].set_adjustment(Gtk.Adjustment(1, 0, 50, 1, 0, 0)) 37 | self.gui["silence-spinbutton"].set_adjustment(Gtk.Adjustment(10, 1, 60, 1, 0, 0)) 38 | self.gui["padding-spinbutton"].set_adjustment(Gtk.Adjustment(5, 1, 50, 1, 0, 0)) 39 | 40 | for key, value in self.config["draw"].items(): 41 | self.gui["%s-spinbutton" % key].set_value(value) 42 | self.gui["%s-spinbutton" % key].connect("value-changed", self.on_draw_spinbutton_changed, key) 43 | 44 | # offset 45 | self.offset_current = None 46 | self.gui["offset-spinbutton"].set_adjustment(Gtk.Adjustment(5, 0, 999, 5, 0, 0)) 47 | 48 | for offset in ("Left", "Right", "Top", "Bottom"): 49 | self.gui["offset-comboboxtext"].append_text(offset) 50 | self.gui["offset-comboboxtext"].connect("changed", self.on_offset_combo_changed) 51 | self.gui["offset-comboboxtext"].set_active(0) 52 | 53 | self.gui["offset-spinbutton"].connect("value-changed", self.on_offset_spinbutton_changed) 54 | 55 | # image source 56 | image_rb = "image-tag-radiobutton" if self.config["image"]["usetag"] else "image-file-radiobutton" 57 | self.gui[image_rb].set_active(True) 58 | 59 | self.gui["image-tag-radiobutton"].connect("notify::active", self.on_image_source_button_switch, True) 60 | self.gui["image-file-radiobutton"].connect("notify::active", self.on_image_source_button_switch, False) 61 | 62 | self.gui["image-open-button"].connect("clicked", self.on_image_open_button_click) 63 | self.gui["save-color-button"].connect("clicked", self.on_save_color_button_click) 64 | self.gui["image-label"].set_text("Image: %s" % name_from_file(self.config["image"]["default"])) 65 | 66 | # autocolor settings 67 | for key in ("saturation", "value"): 68 | self.gui["%s-min-scale" % key].set_adjustment(Gtk.Adjustment(0.5, 0, 1, 0.01, 0, 0)) 69 | self.gui["%s-min-scale" % key].set_value(self.config["autocolor"]["%s_min" % key]) 70 | self.gui["%s-min-scale" % key].connect("value-changed", self.on_autocolor_scale_changed, key) 71 | 72 | for key in ("window", "bands"): 73 | self.gui["ac-%s-spinbutton" % key].set_adjustment(Gtk.Adjustment(5, 0, 1000, 1, 0, 0)) 74 | self.gui["ac-%s-spinbutton" % key].set_value(self.config["autocolor"][key]) 75 | self.gui["ac-%s-spinbutton" % key].connect("value-changed", self.on_autocolor_spinbutton_changed, key) 76 | 77 | # misc 78 | self._mainapp.connect("ac-update", self.on_autocolor_update) 79 | self.gui["refresh-autocolor-button"].connect("clicked", self.on_autocolor_refresh_clicked) 80 | 81 | # actions 82 | auto_action = Gio.SimpleAction.new_stateful( 83 | "autocolor", None, GLib.Variant.new_boolean(self.config["color"]["auto"]) 84 | ) 85 | auto_action.connect("change-state", self.on_autocolor_switch) 86 | self._settings.actions["settings"].add_action(auto_action) 87 | 88 | # action handlers 89 | def on_autocolor_switch(self, action, value): 90 | """Use color analyzer or user preset""" 91 | action.set_state(value) 92 | autocolor = value.get_boolean() 93 | self.config["color"]["auto"] = autocolor 94 | 95 | color = self.config["color"]["autofg"] if autocolor else self.config["color"]["fg"] 96 | self.gui["fg-colorbutton"].set_rgba(color) 97 | self._mainapp.draw.color_update() 98 | 99 | # signal handlers 100 | # noinspection PyUnusedLocal 101 | def on_autocolor_refresh_clicked(self, *args): 102 | if self.config["color"]["auto"]: 103 | self._mainapp.emit("autocolor-refresh", self.config["image"]["usetag"]) 104 | 105 | def on_autocolor_spinbutton_changed(self, button, key): 106 | value = int(button.get_value()) 107 | if key == "window": 108 | value = min(value, self.config["autocolor"]["bands"]) 109 | self.config["autocolor"][key] = value 110 | 111 | def on_autocolor_scale_changed(self, scale, key): 112 | self.config["autocolor"]["%s_min" % key] = scale.get_value() 113 | 114 | def on_fg_color_set(self, button): 115 | key = "autofg" if self.config["color"]["auto"] else "fg" 116 | self.config["color"][key] = button.get_rgba() 117 | self._mainapp.draw.color_update() 118 | 119 | def on_bg_color_set(self, button): 120 | self.config["color"]["bg"] = button.get_rgba() 121 | if self.config["window"]["bgpaint"]: 122 | self._mainapp.canvas.set_bg_rgba(self.config["color"]["bg"]) 123 | 124 | # noinspection PyUnusedLocal 125 | def on_autocolor_update(self, sender, rgba): 126 | self.config["color"]["autofg"] = rgba 127 | if self.config["color"]["auto"]: 128 | self.gui["fg-colorbutton"].set_rgba(rgba) 129 | self._mainapp.draw.color_update() 130 | 131 | def on_draw_spinbutton_changed(self, button, key): 132 | type_ = float if key == "scale" else int 133 | self.config["draw"][key] = type_(button.get_value()) 134 | if key in ("padding", "zero"): 135 | self._mainapp.draw.size_update() 136 | 137 | def on_offset_spinbutton_changed(self, button): 138 | self.config["offset"][self.offset_current] = int(button.get_value()) 139 | self._mainapp.draw.size_update() 140 | 141 | def on_offset_combo_changed(self, combo): 142 | text = combo.get_active_text() 143 | if text is not None: 144 | self.offset_current = text.lower() 145 | self.gui["offset-spinbutton"].set_value(self.config["offset"][self.offset_current]) 146 | 147 | # noinspection PyUnusedLocal 148 | def on_image_source_button_switch(self, button, active, usetag): 149 | if button.get_active(): 150 | self.config["image"]["usetag"] = usetag 151 | self._mainapp.canvas.rebuild_background() 152 | self._mainapp.emit("image-source-switch", usetag) 153 | 154 | # noinspection PyUnusedLocal 155 | def on_image_open_button_click(self, *args): 156 | is_ok, file_ = gtk_open_file(self.window, self.image_filter) 157 | if is_ok: 158 | self.gui["image-label"].set_text("Image: %s" % name_from_file(file_)) 159 | self.config["image"]["default"] = file_ 160 | self._mainapp.emit("default-image-update", file_) 161 | 162 | # noinspection PyUnusedLocal 163 | def on_save_color_button_click(self, *args): 164 | if not self._mainapp.imported.gstreamer or self._mainapp.player.current is None: 165 | logger.warning("No audio file active") 166 | return 167 | 168 | rgba = self.gui["fg-colorbutton"].get_rgba() 169 | color = (rgba.red, rgba.green, rgba.blue) 170 | self._mainapp.palette.add_color(self._mainapp.player.current, color) 171 | -------------------------------------------------------------------------------- /desktop/cavalcade.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Cavalcade 3 | Comment=CAVA GUI 4 | Exec=cavalcade 5 | Terminal=false 6 | Icon=cavalcade 7 | Type=Application 8 | Categories=AudioVideo;Audio;GTK 9 | StartupNotify=false 10 | -------------------------------------------------------------------------------- /desktop/cavalcade.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # W191 - indentation contains tabs 3 | # E251 - unexpected spaces around keyword / parameter equals 4 | # E402 - module level import not at top of file 5 | # E731 - do not assign a lambda expression, use a def 6 | # W503 - line break occurred before a binary operator 7 | ignore = E251, E402, W191, W503 8 | max-line-length = 119 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- Mode: Python; indent-tabs-mode: t; python-indent: 4; tab-width: 4 -*- 3 | 4 | """ 5 | Installation routines. 6 | Install: 7 | $ pip install . 8 | Uninstall: 9 | $ pip uninstall cavalcade 10 | """ 11 | 12 | from setuptools import setup 13 | from cavalcade.version import get_current as get_current_version 14 | 15 | setup( 16 | name = "cavalcade", 17 | version = get_current_version(), 18 | description = "GUI wrapper for C.A.V.A. utility", 19 | license = "GPL-3.0-or-later", 20 | author = "worron", 21 | author_email = "worrongm@gmail.com", 22 | url = "https://github.com/worron/cavalcade", 23 | packages = ["cavalcade", "cavalcade.gui", "cavalcade.data"], 24 | install_requires = ["setuptools"], 25 | package_data = {"cavalcade.gui": ["*.glade", "*.ui"], "cavalcade.data": ["*.ini", "*.svg"]}, 26 | entry_points = { 27 | "console_scripts": ["cavalcade=cavalcade.run:run"], 28 | }, 29 | ) 30 | --------------------------------------------------------------------------------