pax_global_header00006660000000000000000000000064141322416150014511gustar00rootroot0000000000000052 comment=b71543b57d319b0a7d6c54d705ff6cc8e1c0b4e9 jack_mixer-release-17/000077500000000000000000000000001413224161500150345ustar00rootroot00000000000000jack_mixer-release-17/.flake8000066400000000000000000000004151413224161500162070ustar00rootroot00000000000000[flake8] ignore = E116, E265, E266, E731, W503, W504 max-line-length = 99 extend-exclude = build, builddir, data, dist, nsmclient.py per-file-ignores = # import not at top of file due to gi.require_version and i18n app.py: E402 builtins = _ jack_mixer-release-17/.gitignore000066400000000000000000000006761413224161500170350ustar00rootroot00000000000000# binaries jack_mix_box *.lo *.la *.o # generated man pages jack_mix_box.1 jack_mixer.1 # Mixer configuration files *.xml # Geany editor project files *.geany # general Python stuff *.pyc __pycached__/ # distutils / setuptools build/ dist/ *.egg-info # wheels downloaded when build wheel *.whl # Cython stuff _jack_mixer.c _jack_mixer.*.so # recommended meson build directory builddir/ # i18n data/locale/*/LC_MESSAGES/ data/locale/*.po~ jack_mixer-release-17/AUTHORS000066400000000000000000000013011413224161500160770ustar00rootroot00000000000000# Current Maintainer Frédéric Péters # Authors * Nedko Arnaudov (original author) * Christopher Arndt * Arnout Engelen * John Hedges * Olivier Humbert * Sarah Mischke * Frédéric Péters * Daniel Sheeler * Athanasios Silis # Translation ## French * Olivier Humbert ## German * Christopher Arndt ## Spanish Daryl Hanlon # Artwork The jack_mixer application icon has been created by Lapo Calamandrei. jack_mixer-release-17/CHANGELOG.md000066400000000000000000000305441413224161500166530ustar00rootroot00000000000000Change Log ========== ## Version 17 (2021-10-15) Fixed: * Uniform fall rate for kmeter peak indicators across different jack period sizes was enforced (#133). Features: * Install a Ray Session template file that tells Ray Session this version has nsm support (#131). * A Spanish translation was added. * Ability to reset absolute peak meter readings after a user chosen time was added (#135). * Custom slider mouse behavior was changed to mimic that of gtk slider, in particular, the fader no longer jumps to click position but requires a click and drag to move it (#137). * Minimum and maximum width for custom sliders was added. * Minimum and maximum width for meters was added. * Meter redraw period as a user preference was added (#139). * Ability to meter pre-fader signal was added (#97). * Keypad plus and minus accelerator for channel shrink/expand (#141) With contributions from Daniel Sheeler and Christopher Arndt and with Daryl Hanlon providing the Spanish translation. ## Version 16 (2021-04-15) Fixed: * Some global settings were not properly persisted in the settings file when changed in the preferences dialog (#124). * Selecting a custom default project path in the preferences dialog via folder selection widget did not update the path in the text entry. * The message formatting in error dialogs was corrected and when an error dialog is shown, the error message printed to the console now only contains a Python traceback when the debug option is active. * Various debug log messages received minor fixes and improvements. Features: * Internationalization (i18n) support was added, making GUI labels and messages and the command line help translatable. * A German translation was added. * A French translation was added. * A global language setting was added to the preferences. * French and German translations of the application description were added to the XDG desktop file. Documentation: * A man page for `jack_mix_box` was added. * A new [contributing guide](./docs/CONTRIBUTING.md) was added to repository. * The section on environment variables in `jack_mixer's` man page was updated and enhanced. * The NSM project URL was updated in various documents. This release was created by Christopher Arndt with Olivier Humbert providing the French translation. ## Version 15.1 (2021-03-15) **Bugfix release** Fixed: * In fixing issue #81 a regression was introduced in version 15, which caused channel volume levels to not be restored when loading a project XML file. * The `Channel.autoset_*_midi_cc` methods in the Cython extension module didn't return the int result from the C functions they call, causing mis-leading debug log messages. Project infrastructure and internals: * A [wiki] was added to the `jack_mixer` GitHub project and a page with instructions on how to install from source on debian / Ubuntu. * The dependencies for building a Python wheel via `pip` were updated. * When building from a Git checkout, the `cython` program is also found when it is installed as `cython3`. * Debug logging can be enabled by setting the `JACK_MIXER_DEBUG` environment variable (for when the `-d` command line switch can't be used, e.g. when run via NSM). This release was created by Christopher Arndt. [wiki]: https://github.com/jack-mixer/jack_mixer/wiki ## Version 15 (2021-02-25) **Important change:** `jack_mixer` now uses [meson] for building and installation. See [INSTALL.md] for new build instructions. New: * A global setting for default project file path was added and can be changed in the preference dialog. The default value is `$XDG_DATA_HOME/jack_mixer` (which is normally `~/.local/share/jack_mixer`). * A "Recent projects" menu was added, to allow loading recently used / saved projects more quickly. * Direct channel output ports are now optional and can be enabled/disabled in the channel preferences dialog. * Ctrl+left-click on the mute ("M") or solo ("S") channel buttons now activates exclusive mute resp. solo. * A man page for `jack_mixer` was added. * `jack_mix_box` now supports the `-p|--pickup` command line option to enable MIDI pickup mode, to prevent sudden volume or balance value jumps. Fixed: * Activating the solo function on an input channel could cause its output signal be sent to the monitor outputs instead of the signal from the channel, which had monitoring activated. * Volume and balance level and mute and solo state changes originating from the UI now send the correct assigned MIDI CCs, allowing for MIDI feedback to controllers. Same for changes originating from reception of assigned MIDI CCs. * The handler for right-clicking the input channel mute/solo buttons, was accidentally removed and is now re-instated. * Creating a new output channel assigns it a randomly chosen color, which can be changed in the new channel dialog (used to work some releases ago, but was broken at some point). * The `jack_mix_box` command line options `--help` and `--stereo` erroneously required an argument. * Saving the current project on reception of the `SIGUSR1` signal, which is a requirement for LADISH level L1 support, was broken in version 14. * When re-ordering channels via drag-and-drop, the order of the edit / remove channel menu items were not updated. * When creating an output channel, it could happen that the initial channel volume would randomly be set to -inf or 0 dB, regardless of what was selected in new channel dialog. Changed: * The minimum supported Python version is now 3.6. * The `jack_mix_box` command line usage help message was improved. * The channel strip buttons (solo, mute, etc.) now have more distinctive colors when activated or the mouse hovers over them. * The balance slider step size was increased slightly so right-clicking the slider changes the value more rapidly. * When using the "Save as..." function, `jack_mixer` now sets the default filename and directory for file chooser to the last ones used. * A window title was added to the preferences dialog. * MIDI control for mute and solo now interprets control value 0-63 as off and 64-127 as on, instead of toggling the state on reception of any controller value. Project infrastructure and internals: * The `jack_mixer_c` Python extension module, which was originally implemented in hand-written C code using the PYTHON C API, was replaced with the `_jack_mixer` extension module implemented in [Cython], which generates the C code in `_jack_mixer.c`. * The autotools build toolchain was replaced with a build setup using [meson], which improves build times and maintainability markedly. See the file [INSTALL.md] for updated build and installation instructions. * A build option to allow building only `jack_mix_box` was added (`-Dgui=disabled`). * All Python code was re-formatted with [black]. * All errors and warnings reported by [flake8] were fixed or are explicitly and selectively ignored. * The file `version.py` is now generated from the version set in the project definition in the top-level `meson.build` file, leaving this as the only place where the version number needs to be updated before a release. * The `NEWS` file was renamed to `CHANGELOG.md` and converted to Markdown format. This release was created by Christopher Arndt. With a contribution from Athanasios Silis. [INSTALL.md]: ./INSTALL.md [black]: https://pypi.org/project/black/ [Cython]: https://cython.org/ [meson]: https://mesonbuild.com/ [flake8]: https://pypi.org/project/flake8/ ## Version 14 (2020-10-15) * Changes to channel fader/meter layout and features: * Added K20 and K14 scales. * Added tick marks for left/center/right on balance slider and add tooltip displaying left/right value. * Added maximum width for control group labels. Labels are ellipsized if too long and a tooltip with the full name is added. * Channel add/property dialogs usability improvements: * Remember last used settings for new input/outut channel dialogs (MIDI CCs are always initialized with -1 by default, so they can be auto-assigned). * Channel name is pre-filled in with "Input" or "output" and an auto-incremented number suffix. * Add mnemonics for all input/output channel dialog fields. * When running under NSM, closing the main window only hides UI and the "Quit" menu entry is replaced with a "Hide" entry. * Added a global option to always ask for confirmation when quitting jack_mixer. * Allow drag'n'drop to change channel positions. * Added ability to shrink/expand width of input and output channels. * The font color of control group labels automatically adapts to their background color for better contrast and readability. * Fixed: Ctrl-click on volume fader sets it to 0.0 dbFS, not 1.0. * Fixed: some issues with channel monitoring. * Fixed: don't create empty project file on new NSM session. * Fixed: on project load, give input focus to fader of last added channel and deselect volume entry widget so keyboard input doesn't accidentally change the value. With contributions from Christopher Arndt, Daniel Sheeler and Frédéric Péters. ## Version 13 (2020-07-16) * Added NSM support. * Store preferences to per session config file to override global preferences. * Added accelerator shortcuts to menu items. * New ctrl-click, double-click, scroll, and click-drag-anywhere fader behaviors. * Added MIDI 'Pick Up' behavior to avoid discontinuities. * Can choose output channel colors. * Changed to logarithmic ramping on volume changes. * Added a pre/post fader button. * Pick volume for new channels. * Allow manual setting of MIDI control change numbers. * Remove GConf; use plaintext .ini preferences file instead. * Remove remnants of Swig python bindings. With contributions from Daniel Sheeler and Christopher Arndt. ## Version 12 (2020-06-22) * Added reporting of the current volume through SIGUSR1 signal to jack_mix_box. * Reset color of over 0db/NaN peak on click. * Fixed memory leaks. * Fixed some Python 3 compatibility leftovers. With contributions from Daniel Sheeler and Athanasios Silis. ## Version 11 (2020-06-18) * Spread out volume transition over a period of time to reduce discontinuities. * Port to pygobject and GTK3. * Port to Python 3. With contributions from Daniel Sheeler. ## Version 10 (2014-04-27) * Fixed change of channel settings (#18299) * Added a MIDI out port for feeding back volume levels into motorized controllers * Added jack_mix_box, a minimalistic (no UI) jack mixer * Added a trayicon and minimize to tray feature With contributions from John Hedges, Sarah Mischke, and Nedko Arnaudov. ## Version 9 (2010-10-04) * Changed to no longer appends PID to jack client name (#15006) * Added 'Edit .. channel' submenus * Set a default 'apply' button in channel properties * Fixed creation of mono channels * Removed bad crackling when changing the volume through MIDI * Moved back to polling for MIDI events, to avoid the need for threads * Changed to use backward compatible call to gobject.timeout_add (#14999) * Updated not to fail if we can't get lash server name * Added support for Ladish level 1 * Improved SIGUSR1 handling With contributions from Nedko Arnaudov and Arnout Engelen. ## Version 8 (2009-12-16) * Fix private modules lookup * Fix rotation of output channel colours * New menu items to remove output channels * New command line parameter to not connect to LASH ## Version 7 (2009-12-14) * New maintainer, thanks Nedko for everything! * New icon by Lapo Calamandrei * Option to have a gradient in the vumeters * Option to use stock GtkScale widget for volume and balance * Rewrite of the C/Python binding (this removed the dependency on SWIG) * Improve performance when drawing vumeters * New menu items to load/save settings * New "Channel Properties" dialog, allowing to change assigned MIDI CCs * Automatic post fader outputs for input channels * Possibility to add new output channels, besides main mix * New "monitor" output, assignable to any output channel, or input channel (in which case it will take its prefader volume) * Removal of PyXML dependency With contributions from Nedko Arnaudov, Lapo Calamandrei, Arnout Engelen, and Krzysztof Foltman. ## Version 6 (2009-07-25) * Fix building against jack 0.102.20 * Handle python prefix different from install prefix * Fix LASH-less operation * Update install instructions after lash-0.5.3 and phat-0.4.1 releases * Apply Markus patch (thanks!) for sr #1698 (can't restore session using LASH) jack_mixer-release-17/COPYING000066400000000000000000000431031413224161500160700ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. jack_mixer-release-17/INSTALL.md000066400000000000000000000072761413224161500165000ustar00rootroot00000000000000Installation ============ **jack_mixer** uses [meson] and optionally a Python [PEP-517]-compliant build system for building, installation and packaging. ## Requirements Build requirements: * GCC (version 9.x or 10.x recommended) * meson >= 0.54.0[1](#1) * [ninja] * Python headers * [JACK] headers * glib2 headers * gettext * [Cython] (optional, required if building from a Git checkout) * [docutils] (optional, `rst2man` required if building from a Git checkout) Runtime requirements: * Python >= 3.6 * [Pygobject] * [pycairo] * JACK library and server Optional run-time dependencies: * [pyxdg] (for saving your preferences, strongly recommended) * [NSM] (for NSM session management support) The run-time Python dependencies are checked by meson when setting up the build directory. To disable this, use the `-Dcheck-py-modules=false` option to `meson setup.` 1 *meson 0.53.0 also works if you use `ninja compile` and `ninja install` instead of `meson compile` and `meson install`.* ## Building Building with meson always happens in a special build directory. Set up the build in the `builddir` sub-directory and configure the build with: ```console meson setup builddir --prefix=/usr --buildtype=release ``` Then build the software with: ```console meson compile -C builddir ``` **Note:** *For building **jack_mixer** from source on **debian / Ubuntu** derrived Linux distributions, please refer to this [wiki page]. If possible, use your distribution's package manager to install **jack_mixer**.* ## Installation ```console [sudo] meson install -C builddir ``` **Note for packagers**: to install all files under a destination directory other than the filesystem root, set the `DESTDIR` environment variable for `meson install`. For example: ```console DESTDIR="/tmp/jack_mixer-install-root" meson install -C builddir ``` ## Build options There are several project-specific [options] to configure the build and the resulting installation. To see all supported options (including [standard meson options]) and their possible values run: ```console meson configure ``` If you have already set up the build directory, you can append its name to see the current values of all options for this build configuration. To change an option, pass the build directory and `-Doption=value` to `meson setup` or `meson configure`. For example: ```console meson configure builddir -Dgui=disabled ``` ## Building a Python wheel (for maintainers) 1. Make sure you have Python 3, `git` and [pip] installed and your internet connection is online. 2. Run the following command to build a binary wheel: ```console pip wheel . ``` This will automatically download the required build tools, e.g. Cython, meson, ninja, the Python `wheel` package etc. (see the [pyproject.toml] file for details), build the software with meson and then package it into a wheel, which will be placed in the project's root directory. The wheel can be installed with `pip install jack_mixer-*.whl`. [docutils]: https://pypi.org/project/docutils/ [Cython]: https://cython.org/ [JACK]: https://jackaudio.org/ [meson]: https://mesonbuild.com/ [ninja]: https://ninja-build.org/ [NSM]: https://new-session-manager.jackaudio.org/ [options]: https://mesonbuild.com/Build-options.html [pip]: https://pypi.org/project/pip/ [pycairo]: https://pypi.org/project/pycairo/ [PyGObject]: https://pypi.org/project/PyGObject/ [pyxdg]: https://freedesktop.org/wiki/Software/pyxdg/ [PEP-517]: https://www.python.org/dev/peps/pep-0517/ [pyproject.toml]: ./pyproject.toml [standard meson options]: https://mesonbuild.com/Builtin-options.html [wiki page]: https://github.com/jack-mixer/jack_mixer/wiki/Installing-on-debian---Ubuntu jack_mixer-release-17/README.md000066400000000000000000000053271413224161500163220ustar00rootroot00000000000000jack_mixer -- Jack Audio Mixer ============================== **jack_mixer** is a GTK+ JACK audio mixer app with a look & handling similar to hardware mixing desks. It has lot of useful features, apart from being able to mix multiple JACK audio streams. It is licensed under GPL version 2 (or later), check the file [COPYING] for more information. Please visit the project's homepage at https://rdio.space/jackmixer/ for more information. ## Installation To build and install jack_mixer run: ```console meson builddir --prefix=/usr --buildtype=release meson compile -C builddir [sudo] meson install -C builddir ``` Please read the file [INSTALL.md] for more information and requirements. ## Using MIDI CCs to control jack_mixer MIDI Control Change messages (CCs) can be used to control volume, balance/panorama, mute, and solo of input and output channels. The default controllers for added channels are chosen using a predefined algorithm: the first free controller starting from #11, first for volume, next for balance/panorama, then mute and finally solo. So, if you don't delete channels, CC#11 will control the first channel's volume, CC#12 the balance/panorama, CC#13 the mute and CC#14 the solo switch. CC#15 will control the second channel' volume, CC#16 it's balance/panorama, and so on. It is also possible to set other CCs when creating a channel, or afterwards from the channel properties dialog (accessible from the menu or by double clicking on the channel name). MIDI CC values (0-127) are mapped to dBFS using the current slider scale for the corresponding channel. ## Authors and Acknowledgements jack_mixer was initially written and supported by Nedko Arnaudov, it is now maintained by Frédéric Péters. For a list of contributors see the file [AUTHORS]. K-meter implementation taken from jkmeter, licensed under the GPL 2, by Fons Adriaensen. ## Feedback and Contributing If you have trouble getting jack_mixer working, find a bug or you miss some feature, please [create an issue] on GitHub or contact the maintainer by email. You can reach Frédéric at `fpeters (a.t) 0d (dot) be`, and Nedko at `nedko (a.t) arnaudov (dot) name`. Most recently, the primary developers are Daniel Sheeler at `dsheeler (a.t) pobox (dot) com` and Christopher Arndt at `chris (a.t) chrisarndt (dot) de`, and you can also usually find these folks in `#jack_mixer` or `#lad` on FreeNode (as *fpeters*, *nedko*, *dsheeler* and *strogon14*). If you want to get involved with jack_mixer's development, documentation or translation, please read the [contributing guide]. [AUTHORS]: ./AUTHORS [COPYING]: ./COPYING [INSTALL.md]: ./INSTALL.md [contributing guide]: ./docs/CONTRIBUTING.md [create an issue]: https://github.com/jack-mixer/jack_mixer/issues jack_mixer-release-17/data/000077500000000000000000000000001413224161500157455ustar00rootroot00000000000000jack_mixer-release-17/data/art/000077500000000000000000000000001413224161500165335ustar00rootroot00000000000000jack_mixer-release-17/data/art/16x16/000077500000000000000000000000001413224161500173205ustar00rootroot00000000000000jack_mixer-release-17/data/art/16x16/jack_mixer.png000066400000000000000000000010601413224161500221370ustar00rootroot00000000000000PNG  IHDRasBIT|dIDAT8=OTAwJJ@%5DM +-0ďP"PheCcΒd bCtcacq]i&93O7111yIYs;'39>62yH1W`f))mr`3J`)sf"— f12WX^] .c& sPB([]Z e9YZe\"˦݇(`B)8>^ܻ{3ctv]FѬsҧO1, 0CU+Y>~ f}!NEW-EC@D_YUy:"QǿU4T;vMJR6f)OE#ԅue3EEԊ@. XaYNg7eե?8IENDB`jack_mixer-release-17/data/art/16x16/jack_mixer.svg000066400000000000000000000301321413224161500221540ustar00rootroot00000000000000 image/svg+xml Lapo Calamandrei Jack MiXer mixer jack audio Jakub Steiner jack_mixer-release-17/data/art/22x22/000077500000000000000000000000001413224161500173125ustar00rootroot00000000000000jack_mixer-release-17/data/art/22x22/jack_mixer.png000066400000000000000000000016551413224161500221430ustar00rootroot00000000000000PNG  IHDRĴl;sBIT|ddIDAT8OhUs3ψb-$Ūlm-V REFt!Ѵ+.)((j*mcI^bm46ysy/ Ü9; 1ZCD2Q6b=$w;ۺ\b=bu8rzidtS ND1rOYXX}w'!1 QF?S(xǏdqq1J⠆ɲP;rj `UAU5C HeǏdW image/svg+xml Lapo Calamandrei Jack MiXer mixer jack audio Jakub Steiner jack_mixer-release-17/data/art/24x24/000077500000000000000000000000001413224161500173165ustar00rootroot00000000000000jack_mixer-release-17/data/art/24x24/jack_mixer.png000066400000000000000000000017541413224161500221470ustar00rootroot00000000000000PNG  IHDRw=bKGD pHYsHHFk>IDATHǵohUu?9E Ʉm%YQ/.%KD"AX^DDF "VWSPVlbt\nQpÄ"8NmOW$ɽqཻ^/uUݰmi)7,6wq`p̱BO?czzjᖭ1bU ̟LϞnrotcT ͍֫O)@eUի-(C~\iXy1lT_@ym?~2G !9/B<+ލ nY7d 1:\2[+c['sJp&@R1+/a@1FT34Dxkf>ۯw[H 2BL@v;#8Wyq86>3JI9֯}3T˯TZ4+~# 7ޤX(04<Ğ >u l]8¡4c+. }43WMض}+KK8a <-,b"0ֶfadiq Sٹ"7c·(ˍJi5jRh0OLLXgΜ HGnfz:׈.r0jjAuQJX42{:عs"Fwe] Ln\.cZ ^FQJ ٌS(.!w76KK\~1c;BDS,1&KJ0!f 0M?gXcKmDq ۇւa,mH5Q((z}U3y=I±6 1?755 Gz1Mgy%lp=eYF58& ;yo32?c{p[F*%T 5J m740;3G2"֝Ď c"q±.6=0̭OocY+xβe(|rGY\8Xe5 6333кѭ9ׯ]jgC([]AP#}$~ EpeYfٴy˺х#Q_!2~Ν;um$ݘ2L.M~0`Qxe}lh0s{ gyD"d2i7nR.W0.T*$](7<1a[O)JZkR)zR=yZkTFM*k>P5Ѥ45~] A+ t:SKKՊF݊k&^{w=MZkf(܏ԋ?9\;˷.\:#a;dS9;&}'޺/@neC&as7]h10J( n/{pZ_bq|]w-늈='WRZϴUY)|*٫?|_٩/1J#FIKEN>@qH2F"|O_f  W ׳YJiF0㔍ɧdSWP^ !;w;ZwѢpKanQq]i~R=תbMx/~ _(5@U}Gr?DGTFןijMٯccu~ԠIENDB`jack_mixer-release-17/data/art/32x32/jack_mixer.svg000066400000000000000000001214501413224161500221540ustar00rootroot00000000000000 image/svg+xml Lapo Calamandrei Jack MiXer mixer jack audio Jakub Steiner jack_mixer-release-17/data/art/48x48/000077500000000000000000000000001413224161500173325ustar00rootroot00000000000000jack_mixer-release-17/data/art/48x48/jack_mixer.png000066400000000000000000000056751413224161500221710ustar00rootroot00000000000000PNG  IHDR00WsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< :IDATh{\W}?ܹΝ3Y{fgwl'wêmDhDyJ*BEU+@дCUJE-jJ R8<{;:c;ϵiKH3ww~;PJFz '٭7"?:>S#zB:7*C7Xdgr7Ytx ygncz@ywL[NǨU@)P(܊ľq ghp|yemW~*l_2KsŞ<τ@\"Y^Y۶Q W* |y$A+oΞ/dp]T2^8dD$T;{v|" ^] ðzaGfff{̦6Ubv|Wu0:M@Plq?sI%n+Y,,\$]Rbs\; t!o5tuI%*6*3iIE%TB{zmA `<.,]?v>#;vЊF" +f4 L3Z%N`( Ӳa!+Y};Bt)$/;Ozh?IضۺT⦩CT*.駾M7oФ5YͮbFwLNNtL2y`?W/ ]TA@4jbhd2A)WdY0o6%LJ8y|˝c1 jv RGyuI&lb] (O%) hP.1NmAlc}Bi("-Z]7Uv;*b͍M$Tw/? qy&P- A@!n>>s?bkiP\jh4Bl"5 ]\R}*J8)[@a:MbF2>|ԩ[gٱxGBim\t {k/qRLf i ?'`j IY#-4f BYb!|~x_n"vi$ 4)ZOcF BҗYf:(faX LL\LjG44Md5ɨ>yMC81qf:3]ze4F1I9p_uB( F:Pa9\.!6AN h$3`gRBr`];PnwQN$u!T}fpz՛DM2qYwgB9P!RTcwUi:FvVSC{[7A,B CC@TbyeeZ[kkG_ԛ_hDmO4G_#z~sw-,D:Tmw׽ce:M8I!BD :-[۫w}߈EN/}kzH1  rP>n!H{)x^69rw0r 3E$~>ov{(_y^]J09ŏ/S,GRK rz_BjiThaR.xeF<ߌ2yzJƴeTM;M41І)~@b7XАgR F'O=˕ OWճQX*Я𓚦=^T*g@ gsR,. W֛@̦?mT3ƱBZ"ILՎ.p-2}ÿLq2g"|?Fl[ܽ˳Oϱ`ϲ[)#c^s8~`8~qkb0?'{GerWNbV[||Icpdԗq_( 18xx _y:_`(,U XSG|_}=RʇA'i @\->rטþL,[9ʶLzl|A0? shR>LXZ2q:KQBkhD@Iok@A)@A@kL/sξR}e*%Mo?|uǭCK3\OX$ЅD;x]"ޥ@!PVa^VwcTJ7<ە@iMl*4j+aS :IU~Mv!%jMlӛIo&4) 6/^o/PqIENDB`jack_mixer-release-17/data/art/48x48/jack_mixer.svg000066400000000000000000001520671413224161500222020ustar00rootroot00000000000000 image/svg+xml Lapo Calamandrei Jack MiXer mixer jack audio Jakub Steiner jack_mixer-release-17/data/art/scalable/000077500000000000000000000000001413224161500203015ustar00rootroot00000000000000jack_mixer-release-17/data/art/scalable/jack_mixer.svg000066400000000000000000001520671413224161500231510ustar00rootroot00000000000000 image/svg+xml Lapo Calamandrei Jack MiXer mixer jack audio Jakub Steiner jack_mixer-release-17/data/client_templates.xml000066400000000000000000000002151413224161500220210ustar00rootroot00000000000000 jack_mixer-release-17/data/jack_mixer.desktop000066400000000000000000000007771413224161500214670ustar00rootroot00000000000000[Desktop Entry] Name=Jack Mixer Comment=A multi-channel audio mixer application for the JACK Audio Connection Kit Comment[de]=Eine Mehrkanalaudiomixeranwendung für das JACK Audio Connection Kit Comment[es]=Una aplicación para la mezcla de audio multi-canal para JACK Audio Connection Kit Comment[fr]=Une application de mixage audio multi-canal pour le kit de connexion audio JACK Exec=jack_mixer Terminal=false Type=Application StartupNotify=true Categories=GTK;GNOME;AudioVideo;Player;Audio; Icon=jack_mixer jack_mixer-release-17/data/locale/000077500000000000000000000000001413224161500172045ustar00rootroot00000000000000jack_mixer-release-17/data/locale/jack_mixer-de.po000066400000000000000000000613211413224161500222510ustar00rootroot00000000000000# jack_mixer i18n message catalog German translation # # This file is distributed under the same license as the jack_mixer package. # # Copyright (C) 2021 Christopher Arndt # msgid "" msgstr "" "Project-Id-Version: jack_mixer 16\n" "Report-Msgid-Bugs-To: https://github.com/jack-mixer/jack_mixer/issues\n" "POT-Creation-Date: 2021-10-14 12:58+0200\n" "PO-Revision-Date: 2021-03-20 14:27+0100\n" "Last-Translator: Christopher Arndt \n" "Language-Team: German\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: jack_mixer/app.py:48 msgid "" "A multi-channel audio mixer application for the JACK Audio Connection Kit." msgstr "Eine Mehrkanalaudiomixeranwendung für das JACK Audio Connection Kit." #: jack_mixer/app.py:49 msgid "" "jack_mixer is free software; you can redistribute it and/or modify it\n" "under the terms of the GNU General Public License as published by the\n" "Free Software Foundation; either version 2 of the License, or (at your\n" "option) any later version.\n" "\n" "jack_mixer is distributed in the hope that it will be useful, but\n" "WITHOUT ANY WARRANTY; without even the implied warranty of\n" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" "General Public License for more details.\n" "\n" "You should have received a copy of the GNU General Public License along\n" "with jack_mixer; if not, write to the Free Software Foundation, Inc., 51\n" "Franklin Street, Fifth Floor, Boston, MA 02110-130159 USA\n" msgstr "" #: jack_mixer/app.py:174 msgid "jack_mixer XML files" msgstr "jack_mixer XML-Dateien" #: jack_mixer/app.py:187 msgid "_Recent Projects" msgstr "_Zuletzt geöffnet" #: jack_mixer/app.py:230 msgid "_Mixer" msgstr "_Mixer" #: jack_mixer/app.py:232 msgid "_Edit" msgstr "_Bearbeiten" #: jack_mixer/app.py:234 msgid "_Help" msgstr "_Hilfe" #: jack_mixer/app.py:242 msgid "New _Input Channel" msgstr "Neuer _Eingangskanal" #: jack_mixer/app.py:246 msgid "New Output _Channel" msgstr "Neuer _Ausgangskanal" #: jack_mixer/app.py:253 msgid "_Open..." msgstr "Ö_ffnen..." #: jack_mixer/app.py:259 msgid "_Save" msgstr "_Speichern" #: jack_mixer/app.py:263 msgid "Save _As..." msgstr "Speichern _unter..." #: jack_mixer/app.py:268 msgid "_Hide" msgstr "Aus_blenden" #: jack_mixer/app.py:270 msgid "_Quit" msgstr "_Beenden" #: jack_mixer/app.py:277 msgid "_Edit Input Channel" msgstr "_Eingangskanal ändern" #: jack_mixer/app.py:284 msgid "E_dit Output Channel" msgstr "_Ausgangskanal ändern" #: jack_mixer/app.py:291 msgid "_Remove Input Channel" msgstr "Eingangskanal entfe_rnen" #: jack_mixer/app.py:298 msgid "Re_move Output Channel" msgstr "Ausgangskanal e_ntfernen" #: jack_mixer/app.py:305 msgid "Shrink Channels" msgstr "Kanäle ver_kleinern" #: jack_mixer/app.py:309 msgid "Expand Channels" msgstr "Kanäle verbreitern" #: jack_mixer/app.py:322 msgid "_Clear" msgstr "A_lle entfernen" #: jack_mixer/app.py:327 msgid "_Preferences" msgstr "Ein_stellungen" #: jack_mixer/app.py:334 msgid "_About" msgstr "Ü_ber" #: jack_mixer/app.py:384 msgid "Input channel creation failed." msgstr "Erstellen des Eingangskanals fehlgeschlagen" #: jack_mixer/app.py:446 msgid "Output channel creation failed." msgstr "Erstellen des Ausgangskanals fehlgeschlagen" #: jack_mixer/app.py:505 jack_mixer/app.py:594 jack_mixer/app.py:1199 #, python-brace-format msgid "Error loading project file '{filename}': {msg}" msgstr "Projektdatei '{filename}' konnte nicht geladen werden: {msg}" #: jack_mixer/app.py:579 msgid "XML files" msgstr "XML-Dateien" #: jack_mixer/app.py:583 msgid "All files" msgstr "Alle Dateien" #: jack_mixer/app.py:604 msgid "Open project" msgstr "Projekt öffnen" #: jack_mixer/app.py:652 jack_mixer/app.py:695 #, python-brace-format msgid "Error saving project file '{filename}': {msg}" msgstr "Projektdatei '{filename}' konnte nicht gespeichert werden: {msg}" #: jack_mixer/app.py:659 msgid "Save project" msgstr "Projekt speichern" #: jack_mixer/app.py:712 msgid "Quit application?" msgstr "Programm beenden?" #: jack_mixer/app.py:715 msgid "" "All jack_mixer ports will be closed and connections lost,\n" "stopping all sound going through jack_mixer.\n" "\n" "Are you sure?" msgstr "" "Alle Ein- und Ausgänge von jack_mixer werden geschlossen, alle\n" "Verbindungen gehen verloren und der Signalfluss durch jack_mixer wird " "gestoppt.\n" "\n" "Sind Sie sicher?" # Don't translate this unless you want default channel names to be localized #: jack_mixer/app.py:788 msgid "Input" msgstr "" # Don't translate this unless you want default channel names to be localized #: jack_mixer/app.py:791 msgid "Output" msgstr "" #: jack_mixer/app.py:919 msgid "Are you sure you want to clear all channels?" msgstr "Wollen Sie wirklich alle Kanäle entfernen?" #: jack_mixer/app.py:1163 msgid "FILE" msgstr "DATEI" #: jack_mixer/app.py:1164 msgid "load mixer project configuration from FILE" msgstr "Mixerprojektkonfiguration aus DATEI laden" #: jack_mixer/app.py:1171 msgid "enable debug logging messages" msgstr "Debug-Logmeldungen aktivieren" #: jack_mixer/app.py:1175 msgid "NAME" msgstr "NAME" #: jack_mixer/app.py:1178 #, python-format msgid "set JACK client name (default: %(default)s)" msgstr "Setze Namen des JACK-Klienten (Standard: %(default)s)" #: jack_mixer/app.py:1189 msgid "" "Mixer creation failed:\n" "\n" "{}" msgstr "" "Erstellen einer Mixer-Instanz fehlgeschlagen:\n" "\n" "{}" #: jack_mixer/channel.py:115 jack_mixer/channel.py:1395 msgid "M" msgstr "M" #: jack_mixer/channel.py:124 msgid "MON" msgstr "MON" #: jack_mixer/channel.py:134 msgid "PRE" msgstr "" #: jack_mixer/channel.py:136 msgid "Pre-fader (on) / Post-fader (off) metering" msgstr "Pegelanzeige Prä- (ein) / Post-Fader (aus)" #: jack_mixer/channel.py:630 msgid "S" msgstr "S" #: jack_mixer/channel.py:641 msgid "Cannot create a channel" msgstr "Erzeugung eines Kanals fehlgeschlagen" #: jack_mixer/channel.py:858 msgid "Cannot create an output channel" msgstr "Erzeugung eines Ausgangkanals fehlgeschlagen" #: jack_mixer/channel.py:1006 #, python-brace-format msgid "Channel '{name}' Properties" msgstr "Eigenschaften von Kanal '{name}'" #: jack_mixer/channel.py:1047 msgid "Properties" msgstr "Eigenschaften" #: jack_mixer/channel.py:1052 msgid "_Name" msgstr "_Name" #: jack_mixer/channel.py:1061 msgid "Mode" msgstr "Modus" #: jack_mixer/channel.py:1062 msgid "_Mono" msgstr "_Mono" #: jack_mixer/channel.py:1063 msgid "_Stereo" msgstr "_Stereo" #: jack_mixer/channel.py:1068 msgid "MIDI Control Changes" msgstr "MIDI Control Changes" #: jack_mixer/channel.py:1074 #, python-brace-format msgid "" "{param} MIDI Control Change number (0-127, set to -1 to assign next free CC " "#)" msgstr "" "MIDI Control Change Nummer für {param} (0-127, auf -1 setzen, um nächste " "freie CC# zuzuweisen)" #: jack_mixer/channel.py:1076 msgid "_Volume" msgstr "_Lautstärke" #: jack_mixer/channel.py:1080 msgid "Volume" msgstr "Lautstärke" #: jack_mixer/channel.py:1083 jack_mixer/channel.py:1094 #: jack_mixer/channel.py:1105 jack_mixer/channel.py:1130 msgid "Learn" msgstr "Lerne" #: jack_mixer/channel.py:1087 msgid "_Balance" msgstr "_Balance" #: jack_mixer/channel.py:1091 msgid "Balance" msgstr "Balance" #: jack_mixer/channel.py:1098 msgid "M_ute" msgstr "St_umm" #: jack_mixer/channel.py:1102 msgid "Mute" msgstr "Stumm" #: jack_mixer/channel.py:1112 msgid "_Direct Out(s)" msgstr "Direktausgänge" #: jack_mixer/channel.py:1117 msgid "Add direct post-fader output(s) for channel." msgstr "Kanal hat direkte(n) post-fader Ausgang/-gänge" #: jack_mixer/channel.py:1123 msgid "S_olo" msgstr "S_olo" #: jack_mixer/channel.py:1127 msgid "Solo" msgstr "Solo" #: jack_mixer/channel.py:1164 msgid "Please move the MIDI control you want to use for this function." msgstr "" "Bitte bewegen Sie das MIDI-Kontrollelement, dass Sie dieser Funktion " "zuweisen wollen." #: jack_mixer/channel.py:1167 #, fuzzy msgid "This window will close in 5 seconds." msgstr "Dieses Fenster schließt in 5 Sekunden." #: jack_mixer/channel.py:1173 #, python-brace-format msgid "This window will close in {seconds} seconds." msgstr "Dieses Fenster schließt in {seconds} Sekunden." #: jack_mixer/channel.py:1254 msgid "Value" msgstr "Initialwert" #: jack_mixer/channel.py:1255 msgid "-_Inf" msgstr "-_Inf" #: jack_mixer/channel.py:1256 msgid "_0dB" msgstr "_0dB" #: jack_mixer/channel.py:1263 msgid "New Input Channel" msgstr "Neuer Eingangskanal" #: jack_mixer/channel.py:1296 msgid "_Color" msgstr "_Farbe" #: jack_mixer/channel.py:1305 msgid "Input Channels" msgstr "Eingangskanäle" #: jack_mixer/channel.py:1307 msgid "_Display solo buttons" msgstr "Solo-Schalter anzeigen" #: jack_mixer/channel.py:1329 msgid "New Output Channel" msgstr "Neuer Ausgangskanal" #: jack_mixer/channel.py:1397 msgid "Mute output channel send" msgstr "Sendeweg zum Ausgangskanal stummschalten" #: jack_mixer/channel.py:1403 msgid "Solo output send" msgstr "Sendeweg zum Ausgangskanal auf Solo schalten" #: jack_mixer/channel.py:1407 msgid "P" msgstr "P" #: jack_mixer/channel.py:1409 msgid "Pre (on) / Post (off) fader send" msgstr "Prä (ein) / Post-Fader (aus) Ausleitung zum Sendeweg" #: jack_mixer/gui.py:47 msgid "Use system setting" msgstr "Systemeinstellung benutzen" #: jack_mixer/gui.py:70 msgid "Cannot load PyXDG. " msgstr "Kann PyXDG nicht laden. " #: jack_mixer/gui.py:71 msgid "Your preferences will not be preserved across jack_mixer invocations." msgstr "" "Ihre globalen Einstellungen bleiben nicht erhalten wenn jack_mixer beendet " "wird." #: jack_mixer/gui.py:173 #, fuzzy, python-format msgid "Ignoring default_meter_scale setting, because '%s' scale is not known." msgstr "" "Die Einstellung für default_meter_scale wird ignoriert, da die Skala '%s' " "unbekannt ist." #: jack_mixer/gui.py:184 #, fuzzy, python-format msgid "Ignoring default_slider_scale setting, because '%s' scale is not known." msgstr "" "Die Einstellung für default_slider_scale wird ignoriert, da die Skala '%s' " "unbekannt ist." #: jack_mixer/preferences.py:30 msgid "Preferences" msgstr "Einstellungen" #: jack_mixer/preferences.py:53 msgid "" "Set the path where mixer project files are saved and loaded from by default" msgstr "" "Setze den Pfad, unter dem Mixer-Projektdateien standardmäßig gespeichert und " "von dem sie geladen werden" #: jack_mixer/preferences.py:59 jack_mixer/preferences.py:71 msgid "Default Project Path" msgstr "Standardprojektpfad" #: jack_mixer/preferences.py:76 msgid "Set the interface language and localisation" msgstr "Setze die Sprache und Lokalisierung die Benutzerschnittstelle " #: jack_mixer/preferences.py:80 msgid "Language:" msgstr "Sprache: " #: jack_mixer/preferences.py:83 msgid "Confirm quit" msgstr "Beenden bestätigen" #: jack_mixer/preferences.py:85 msgid "Always ask for confirmation before quitting the application" msgstr "Das Beenden des Programms muss immer bestätigt werden" #: jack_mixer/preferences.py:91 msgid "Use custom widgets" msgstr "Angepasste Kontrollelemente verwenden" #: jack_mixer/preferences.py:93 msgid "Use widgets with custom design for the channel sliders" msgstr "" "Benutze Kontrollelemente mit angepasstem Design für die Channel-Schieber" #: jack_mixer/preferences.py:99 msgid "Draw the volume meters with the selected solid color" msgstr "Zeichne die Pegelanzeigen einheitlich mit der ausgewählten Farbe" #: jack_mixer/preferences.py:100 msgid "Use custom vumeter color" msgstr "Festgelegte Farbe für Pegelanzeige verwenden" #: jack_mixer/preferences.py:113 msgid "Custom color:" msgstr "Farbe:" #: jack_mixer/preferences.py:121 msgid "Reset the peak meters after the specified time" msgstr "Setze die Spitzenpegelmarkierung nach der angegebenen Zeit zurück" #: jack_mixer/preferences.py:122 msgid "Auto reset peak meter" msgstr "Spitzenpegel automatisch zurücksetzen" #: jack_mixer/preferences.py:139 msgid "Time (s):" msgstr "Zeit (s):" #: jack_mixer/preferences.py:149 msgid "" "Update the volume level meters with the specified interval in milliseconds" msgstr "" "Aktualisiere die Laustärkenpegelanzeige im angegebenen Interval in " "Millisekunden" #: jack_mixer/preferences.py:152 msgid "Meter Refresh Period (ms):" msgstr "Auffrischinterval Pegelanzeige (ms):" #: jack_mixer/preferences.py:158 msgid "Interface" msgstr "Benutzerschnittstelle" #: jack_mixer/preferences.py:164 msgid "Set the scale for all volume meters" msgstr "Setze die Skala für alle Lautstärkenpegelanzeigen" #: jack_mixer/preferences.py:165 msgid "Meter scale:" msgstr "Meterskala:" #: jack_mixer/preferences.py:172 msgid "Set the scale for all volume sliders" msgstr "Setze die Skala für alle Lautstärkenschieber" #: jack_mixer/preferences.py:173 msgid "Slider scale:" msgstr "Schieberskala:" #: jack_mixer/preferences.py:180 msgid "Scales" msgstr "Skalen" #: jack_mixer/preferences.py:187 msgid "" "Set how channel volume and balance are controlled via MIDI:\n" "\n" "- Jump To Value: channel volume or balance is set immediately to received " "controller value\n" "- Pick Up: control changes are ignored until a controller value near the " "current value is received\n" msgstr "" "Bestimme, wie Kanallautstärke und -balance über MIDI kontrolliert werden:\n" "\n" "- Jump To Value: die Kanallautstärke oder -balance wird unmittelbar auf den " "empfangenen Kontrollerwert gesetzt\n" "- Pick Up: Control Changes werden ignoriert bis ein Kontrollerwert nahe des " "aktuellen Werts empfangen wird\n" #: jack_mixer/preferences.py:191 msgid "Control Behavior:" msgstr "Kontrollverhalten:" #: jack_mixer/preferences.py:198 msgid "MIDI" msgstr "MIDI" #: jack_mixer/preferences.py:309 msgid "You need to restart the application for this setting to take effect." msgstr "" "Sie müssen die Applikation neu starten, damit diese Einstellung wirksam wird." #: jack_mixer/scale.py:88 msgid "" "IEC 60268-18 Peak programme level meters - Digital audio peak level meter" msgstr "IEC 60268-18 Aussteuerungsmesser - Digitaler Audio-Spitzenpegelmesser" #: jack_mixer/scale.py:115 msgid "" "IEC 60268-18 Peak programme level meters - Digital audio peak level meter, " "fewer marks" msgstr "" "IEC 60268-18 Aussteuerungsmesser - Digitaler Audio-Spitzenpegelmesser mit " "weniger Eichmarken" #: jack_mixer/scale.py:135 msgid "Linear scale with range from -70 to 0 dBFS" msgstr "Lineare Skala mit einem Bereich von -70 bis 0 dbFS" #: jack_mixer/scale.py:156 msgid "Linear scale with range from -30 to +30 dBFS" msgstr "Lineare Skala mit einem Bereich von -30 bis +30 dBFS" #: jack_mixer/scale.py:167 msgid "K20 scale" msgstr "K20 Skala" #: jack_mixer/scale.py:207 msgid "K14 scale" msgstr "K14 Skala" #: jack_mixer/serialization_xml.py:58 #, python-brace-format msgid "Document type '{type}' not supported." msgstr "Dokumententyp '{type}' wird nicht unterstützt." #: jack_mixer/slider.py:260 msgid "Center" msgstr "Mitte" #: jack_mixer/slider.py:263 #, python-brace-format msgid "Left: {left} / Right: {right}" msgstr "Links: {left} / Rechts: {right}" #: /usr/lib/python3.9/argparse.py:296 msgid "usage: " msgstr "Benutzung: " #: /usr/lib/python3.9/argparse.py:856 msgid ".__call__() not defined" msgstr ".__call__() ist nicht definiert" #: /usr/lib/python3.9/argparse.py:1199 #, python-format msgid "unknown parser %(parser_name)r (choices: %(choices)s)" msgstr "unbekannter Parser %(parser_name)r (Auswahl: %(choices)s)" #: /usr/lib/python3.9/argparse.py:1259 #, python-format msgid "argument \"-\" with mode %r" msgstr "Argument \"-\" in Kombination mit Modus %r" #: /usr/lib/python3.9/argparse.py:1268 #, python-format msgid "can't open '%(filename)s': %(error)s" msgstr "Kann '%(filename)s' nicht öffnen: %(error)s" #: /usr/lib/python3.9/argparse.py:1477 #, python-format msgid "cannot merge actions - two groups are named %r" msgstr "Kann die Aktionen nicht zusammenführen - es git zwei Gruppen namens %r" #: /usr/lib/python3.9/argparse.py:1515 msgid "'required' is an invalid argument for positionals" msgstr "'required' ist als Argument für positionale Argumente nicht gültig" #: /usr/lib/python3.9/argparse.py:1537 #, python-format msgid "" "invalid option string %(option)r: must start with a character " "%(prefix_chars)r" msgstr "" "ungültiger Optionsbezeichner %(option)r: muss mit einem %(prefix_chars)r " "anfangen" #: /usr/lib/python3.9/argparse.py:1555 #, python-format msgid "dest= is required for options like %r" msgstr "dest= ist für Optionen der Art %r notwendig" #: /usr/lib/python3.9/argparse.py:1572 #, python-format msgid "invalid conflict_resolution value: %r" msgstr "ungültiger Wert für conflict_resolution: %r" #: /usr/lib/python3.9/argparse.py:1590 #, python-format msgid "conflicting option string: %s" msgid_plural "conflicting option strings: %s" msgstr[0] "Konflikt mit Optionsbezeichner: %s" msgstr[1] "Konflikt mit Optionsbezeichnern: %s" #: /usr/lib/python3.9/argparse.py:1656 msgid "mutually exclusive arguments must be optional" msgstr "Sich gegenseitig auschließende Argumente müssen optional sein" #: /usr/lib/python3.9/argparse.py:1723 msgid "positional arguments" msgstr "Positionsargumente" #: /usr/lib/python3.9/argparse.py:1724 msgid "optional arguments" msgstr "Optionale Argumente" #: /usr/lib/python3.9/argparse.py:1739 msgid "show this help message and exit" msgstr "Diese Hilfe anzeigen und beenden" #: /usr/lib/python3.9/argparse.py:1770 msgid "cannot have multiple subparser arguments" msgstr "mehrere subparser Argumente sind nicht erlaubt" #: /usr/lib/python3.9/argparse.py:1822 /usr/lib/python3.9/argparse.py:2333 #, python-format msgid "unrecognized arguments: %s" msgstr "Nicht erkannte Argumente: %s" #: /usr/lib/python3.9/argparse.py:1923 #, python-format msgid "not allowed with argument %s" msgstr "ist in Kombination mit Argument %s nicht erlaubt" #: /usr/lib/python3.9/argparse.py:1969 /usr/lib/python3.9/argparse.py:1983 #, python-format msgid "ignored explicit argument %r" msgstr "das explizite Argument %r wurde ignoriert" #: /usr/lib/python3.9/argparse.py:2090 #, python-format msgid "the following arguments are required: %s" msgstr "Die folgenden Argument sind erforderlich: %s" #: /usr/lib/python3.9/argparse.py:2105 #, python-format msgid "one of the arguments %s is required" msgstr "eins der Argumente %s wird erwartet" #: /usr/lib/python3.9/argparse.py:2148 msgid "expected one argument" msgstr "ein Argument erwartet" #: /usr/lib/python3.9/argparse.py:2149 msgid "expected at most one argument" msgstr "höchstens ein Argument erwartet" #: /usr/lib/python3.9/argparse.py:2150 msgid "expected at least one argument" msgstr "mindestens ein Argument erwartet" #: /usr/lib/python3.9/argparse.py:2154 #, python-format msgid "expected %s argument" msgid_plural "expected %s arguments" msgstr[0] "%s Argument erwartet" msgstr[1] "%s Argumente erwartet" #: /usr/lib/python3.9/argparse.py:2212 #, python-format msgid "ambiguous option: %(option)s could match %(matches)s" msgstr "mehrdeutige Option: konnte %(option)s nicht zu %(matches)s zuordnen" #: /usr/lib/python3.9/argparse.py:2276 #, python-format msgid "unexpected option string: %s" msgstr "unerwarteter Optionsbezeichner: %s" #: /usr/lib/python3.9/argparse.py:2473 #, python-format msgid "%r is not callable" msgstr "%r ist nicht aufrufbar" #: /usr/lib/python3.9/argparse.py:2490 #, python-format msgid "invalid %(type)s value: %(value)r" msgstr "ungültiger Wert für %(type)s: %(value)r" #: /usr/lib/python3.9/argparse.py:2501 #, python-format msgid "invalid choice: %(value)r (choose from %(choices)s)" msgstr "ungültige Auswahl: %(value)r (wähle aus %(choices)s)" #: /usr/lib/python3.9/argparse.py:2577 #, python-format msgid "%(prog)s: error: %(message)s\n" msgstr "%(prog)s: Fehler: %(message)s\n" #: src/jack_mix_box.c:50 msgid "" "Usage: jack_mix_box [-n ] [-p] [-s] [-v ] MIDI_CC...\n" "\n" "-h|--help print this help message\n" "-n|--name set JACK client name\n" "-p|--pickup enable MIDI pickup mode (default: jump-to-value)\n" "-s|--stereo make all input channels stereo with left+right input\n" "-v|--volume initial volume gain in dBFS (default 0.0, i.e. unity gain)\n" "\n" "Each positional argument is interpreted as a MIDI Control Change number and\n" "adds a mixer channel with one (mono) or left+right (stereo) inputs, whose\n" "volume can be controlled via the given MIDI Control Change.\n" "\n" "Send SIGUSR1 to the process to have the current volumes reported per input\n" "channel.\n" "\n" msgstr "" "Benutzung: jack_mix_box [-n ] [-p] [-s] [-v ] MIDI_CC...\n" "\n" "-h|--help Gebe diesen Hilfstext aus\n" "-n|--name Setze Namen des JACK-Klienten\n" "-p|--pickup MIDI Pickup-Modus einschalten (Standard: jump-to-value)\n" "-s|--stereo Alle Eingangskanäle sind Stereo mit Eingängen für Links+Rechts\n" "-v|--volume Startwert für Laustärke in dBFS (Standard: 0,0 - i.e.\n" " Verstärkungsfaktor 1)\n" "\n" "Jedes Positionsargument wird als eine Control Change Nummer interpretiert\n" "und fügt einen Mixer-Kanal mit einem (Mono) oder linkem+rechtem (Stereo)\n" "Eingang hinzu, dessen Lautstärke mit dem angegebenen MIDI Control Change\n" "kontrolliert werden kann.\n" "\n" "Wenn der Prozess ein SIGUSR1 Signal empfängt, werden die momentanen\n" "Lautstärkewerte pro Kanal ausgegeben.\n" "\n" #: src/jack_mix_box.c:134 #, c-format msgid "Unknown argument, aborting.\n" msgstr "Unbekanntes Argument, breche ab.\n" #: src/jack_mix_box.c:140 msgid "You must specify at least one input channel.\n" msgstr "Sie müssen mindestens einen Eingangskanal angeben.\n" #: src/jack_mix_box.c:176 #, c-format msgid "Failed to add channel %d, aborting.\n" msgstr "Konnte Kanal %d nicht hinzufügen, breche ab.\n" #. JACK_MIXER_NO_ERROR #: src/jack_mixer.c:222 msgid "No error.\n" msgstr "Kein Fehler.\n" #. JACK_MIXER_ERROR_JACK_CLIENT_CREATE #: src/jack_mixer.c:224 msgid "" "Could not create JACK client.\n" "Please make sure JACK daemon is running.\n" msgstr "" "Erstellen eines JACK-Klienten fehlgeschlagen.\n" "Bitte stellen Sie sicher, dass der JACK Server läuft.\n" #. JACK_MIXER_ERROR_JACK_MIDI_IN_CREATE #: src/jack_mixer.c:226 msgid "Could not create JACK MIDI in port.\n" msgstr "Erstellen eines JACK-MIDI Eingabeports fehlgeschlagen.\n" #. JACK_MIXER_ERROR_JACK_MIDI_OUT_CREATE #: src/jack_mixer.c:228 msgid "Could not create JACK MIDI out port.\n" msgstr "Erstellen eines JACK-MIDI Ausgabeports fehlgeschlagen.\n" #. JACK_MIXER_ERROR_JACK_SET_PROCESS_CALLBACK #: src/jack_mixer.c:230 msgid "Could not set JACK process callback.\n" msgstr "Setzen des JACK-Prozess-Callback fehlgeschlagen.\n" #. JACK_MIXER_ERROR_JACK_SET_BUFFER_SIZE_CALLBACK #: src/jack_mixer.c:232 #, fuzzy msgid "Could not set JACK buffer size callback.\n" msgstr "Setzen des JACK-Prozess-Callback fehlgeschlagen.\n" #. JACK_MIXER_ERROR_JACK_ACTIVATE #: src/jack_mixer.c:234 msgid "Could not activate JACK client.\n" msgstr "Aktivieren des JACK-Klients fehlgeschlagen.\n" #. JACK_MIXER_ERROR_CHANNEL_MALLOC #: src/jack_mixer.c:236 msgid "Could not allocate memory for channel.\n" msgstr "Speicherallozierung für den Kanal fehlgeschlagen.\n" #. JACK_MIXER_ERROR_CHANNEL_NAME_MALLOC #: src/jack_mixer.c:238 msgid "Could not allocate memory for channel name.\n" msgstr "Speicherallozierung für den Kanalnamen fehlgeschlagen.\n" #. JACK_MIXER_ERROR_PORT_REGISTER #: src/jack_mixer.c:240 msgid "Could not register JACK port for channel.\n" msgstr "Registrierung des JACK-Ports für den Kanal fehlgeschlagen.\n" #. JACK_MIXER_ERROR_PORT_REGISTER_LEFT #: src/jack_mixer.c:242 msgid "Could not register JACK port for left channel.\n" msgstr "Registrierung des JACK-Ports für den linken Kanal fehlgeschlagen.\n" #. JACK_MIXER_ERROR_PORT_REGISTER_RIGHT #: src/jack_mixer.c:244 msgid "Could not register JACK port for right channel.\n" msgstr "Registrierung des JACK-Ports für den rechten Kanal fehlgeschlagen.\n" #. JACK_MIXER_ERROR_JACK_RENAME_PORT #: src/jack_mixer.c:246 msgid "Could not rename JACK port for channel.\n" msgstr "Umbenennung des JACK-Ports des Kanals fehlgeschlagen.\n" #. JACK_MIXER_ERROR_JACK_RENAME_PORT_LEFT #: src/jack_mixer.c:248 msgid "Could not rename JACK port for left channel.\n" msgstr "Umbenennung des JACK-Ports des linken Kanals fehlgeschlagen.\n" #. JACK_MIXER_ERROR_JACK_RENAME_PORT_LEFT #: src/jack_mixer.c:250 msgid "Could not rename JACK port for right channel.\n" msgstr "Umbenennung des JACK-Ports des rechten Kanals fehlgeschlagen.\n" #. JACK_MIXER_ERROR_PORT_NAME_MALLOC #: src/jack_mixer.c:252 msgid "Could not allocate memory for port name.\n" msgstr "Speicherallozierung für den Portnamen fehlgeschlagen.\n" #. JACK_MIXER_ERROR_INVALID_CC #: src/jack_mixer.c:254 msgid "Control Change number out of range.\n" msgstr "Control Change Nummer außerhalb des gültigen Bereichs.\n" #. JACK_MIXER_ERROR_NO_FREE_CC #: src/jack_mixer.c:256 msgid "No free Control Change number.\n" msgstr "Keine freie Control Change Nummer.\n" #: src/jack_mixer.c:811 #, c-format msgid "%s: volume is %f dbFS for mixer channel: %s\n" msgstr "%s: Lautstärke des Mixerkanals %s ist %f dbFS\n" jack_mixer-release-17/data/locale/jack_mixer-es.po000066400000000000000000000602171413224161500222730ustar00rootroot00000000000000# jack_mixer i18n message catalog template # # This file is distributed under the same license as the jack_mixer package. # # Copyright (C) 2021 Christopher Arndt # msgid "" msgstr "" "Project-Id-Version: jack_mixer 16\n" "Report-Msgid-Bugs-To: https://github.com/jack-mixer/jack_mixer/issues\n" "POT-Creation-Date: 2021-10-14 12:58+0200\n" "PO-Revision-Date: 2021-10-14 14:08+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.3\n" #: jack_mixer/app.py:48 msgid "" "A multi-channel audio mixer application for the JACK Audio Connection Kit." msgstr "" "Una aplicación para la mezcla de audio multi-canal para JACK Audio " "Connection Kit." #: jack_mixer/app.py:49 msgid "" "jack_mixer is free software; you can redistribute it and/or modify it\n" "under the terms of the GNU General Public License as published by the\n" "Free Software Foundation; either version 2 of the License, or (at your\n" "option) any later version.\n" "\n" "jack_mixer is distributed in the hope that it will be useful, but\n" "WITHOUT ANY WARRANTY; without even the implied warranty of\n" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" "General Public License for more details.\n" "\n" "You should have received a copy of the GNU General Public License along\n" "with jack_mixer; if not, write to the Free Software Foundation, Inc., 51\n" "Franklin Street, Fifth Floor, Boston, MA 02110-130159 USA\n" msgstr "" "jack_mixer is free software; you can redistribute it and/or modify it\n" "under the terms of the GNU General Public License as published by the\n" "Free Software Foundation; either version 2 of the License, or (at your\n" "option) any later version.\n" "\n" "jack_mixer is distributed in the hope that it will be useful, but\n" "WITHOUT ANY WARRANTY; without even the implied warranty of\n" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" "General Public License for more details.\n" "\n" "You should have received a copy of the GNU General Public License along\n" "with jack_mixer; if not, write to the Free Software Foundation, Inc., 51\n" "Franklin Street, Fifth Floor, Boston, MA 02110-130159 USA\n" #: jack_mixer/app.py:174 msgid "jack_mixer XML files" msgstr "Archivos XML de jack_mixer" #: jack_mixer/app.py:187 msgid "_Recent Projects" msgstr "_Proyectos Recientes" #: jack_mixer/app.py:230 msgid "_Mixer" msgstr "_Mezclador" #: jack_mixer/app.py:232 msgid "_Edit" msgstr "_Editar" #: jack_mixer/app.py:234 msgid "_Help" msgstr "_Ayuda" #: jack_mixer/app.py:242 msgid "New _Input Channel" msgstr "Nuevo Canal de _Entrada" #: jack_mixer/app.py:246 msgid "New Output _Channel" msgstr "Nuevo _Canal de Salida" #: jack_mixer/app.py:253 msgid "_Open..." msgstr "_Abrir..." #: jack_mixer/app.py:259 msgid "_Save" msgstr "_Guardar" #: jack_mixer/app.py:263 msgid "Save _As..." msgstr "Guardar _Como..." #: jack_mixer/app.py:268 msgid "_Hide" msgstr "_Ocultar" #: jack_mixer/app.py:270 msgid "_Quit" msgstr "_Salir" #: jack_mixer/app.py:277 msgid "_Edit Input Channel" msgstr "_Editar Canal Entrada" #: jack_mixer/app.py:284 msgid "E_dit Output Channel" msgstr "E_ditar Canal Salida" #: jack_mixer/app.py:291 msgid "_Remove Input Channel" msgstr "E_liminar Canal Entrada" #: jack_mixer/app.py:298 msgid "Re_move Output Channel" msgstr "Eli_minar Canal Salida" #: jack_mixer/app.py:305 msgid "Shrink Channels" msgstr "Encoger Canales" #: jack_mixer/app.py:309 msgid "Expand Channels" msgstr "Expandir Canales" #: jack_mixer/app.py:322 msgid "_Clear" msgstr "_Va_ciar" #: jack_mixer/app.py:327 msgid "_Preferences" msgstr "_Preferencias" #: jack_mixer/app.py:334 msgid "_About" msgstr "_Acerca de" #: jack_mixer/app.py:384 msgid "Input channel creation failed." msgstr "Error al crear canal de entrada." #: jack_mixer/app.py:446 msgid "Output channel creation failed." msgstr "Error al crear canal de salida." #: jack_mixer/app.py:505 jack_mixer/app.py:594 jack_mixer/app.py:1199 #, python-brace-format msgid "Error loading project file '{filename}': {msg}" msgstr "Error al cargar el archivo de proyecto '{filename}': {msg}" #: jack_mixer/app.py:579 msgid "XML files" msgstr "Archivos XML" #: jack_mixer/app.py:583 msgid "All files" msgstr "Todos los archivos" #: jack_mixer/app.py:604 msgid "Open project" msgstr "Abrir proyecto" #: jack_mixer/app.py:652 jack_mixer/app.py:695 #, python-brace-format msgid "Error saving project file '{filename}': {msg}" msgstr "Error al guardar el archivo de proyecto '{filename}': {msg}" #: jack_mixer/app.py:659 msgid "Save project" msgstr "Guardar proyecto" #: jack_mixer/app.py:712 msgid "Quit application?" msgstr "¿Salir de la aplicación?" #: jack_mixer/app.py:715 msgid "" "All jack_mixer ports will be closed and connections lost,\n" "stopping all sound going through jack_mixer.\n" "\n" "Are you sure?" msgstr "" "Todos los puertos de jack_mixer se cerrarán y se perderán las conexiones,\n" "deteniendo cualquier audio procesado por jack_mixer.\n" "\n" "¿Deseas seguir?" #: jack_mixer/app.py:788 msgid "Input" msgstr "Entrada" #: jack_mixer/app.py:791 msgid "Output" msgstr "Salida" #: jack_mixer/app.py:919 msgid "Are you sure you want to clear all channels?" msgstr "¿Deseas seguir y eliminar todos los canales?" #: jack_mixer/app.py:1163 msgid "FILE" msgstr "ARCHIVO" #: jack_mixer/app.py:1164 msgid "load mixer project configuration from FILE" msgstr "cargar configuración del mezclador de ARCHIVO" #: jack_mixer/app.py:1171 msgid "enable debug logging messages" msgstr "habilitar mensajes de registro de debug" #: jack_mixer/app.py:1175 msgid "NAME" msgstr "NOMBRE" #: jack_mixer/app.py:1178 #, python-format msgid "set JACK client name (default: %(default)s)" msgstr "establecer nombre de cliente de JACK (predeterminado: %(default)s)" #: jack_mixer/app.py:1189 msgid "" "Mixer creation failed:\n" "\n" "{}" msgstr "" "Error al crear mezclador:\n" "\n" "{}" #: jack_mixer/channel.py:115 jack_mixer/channel.py:1395 msgid "M" msgstr "M" #: jack_mixer/channel.py:124 msgid "MON" msgstr "MON" #: jack_mixer/channel.py:134 msgid "PRE" msgstr "PRE" #: jack_mixer/channel.py:136 msgid "Pre-fader (on) / Post-fader (off) metering" msgstr "Medición Pre-fader (encendido) / Post-fader (apagado)" #: jack_mixer/channel.py:630 msgid "S" msgstr "S" #: jack_mixer/channel.py:641 msgid "Cannot create a channel" msgstr "No se pudo crear un canal" #: jack_mixer/channel.py:858 msgid "Cannot create an output channel" msgstr "No se pudo crear un canal de salida" #: jack_mixer/channel.py:1006 #, python-brace-format msgid "Channel '{name}' Properties" msgstr "Propiedades del Canal '{name}'" #: jack_mixer/channel.py:1047 msgid "Properties" msgstr "Propiedades" #: jack_mixer/channel.py:1052 msgid "_Name" msgstr "_Nombre" #: jack_mixer/channel.py:1061 msgid "Mode" msgstr "Modo" #: jack_mixer/channel.py:1062 msgid "_Mono" msgstr "_Mono" #: jack_mixer/channel.py:1063 msgid "_Stereo" msgstr "E_stéreo" #: jack_mixer/channel.py:1068 msgid "MIDI Control Changes" msgstr "MIDI Control Changes" #: jack_mixer/channel.py:1074 #, python-brace-format msgid "" "{param} MIDI Control Change number (0-127, set to -1 to assign next free CC " "#)" msgstr "" "{param} Número de MIDI Control Change (0-127, establecido en -1 para asignar " "el siguiente número CC libre)" #: jack_mixer/channel.py:1076 msgid "_Volume" msgstr "_Volumen" #: jack_mixer/channel.py:1080 msgid "Volume" msgstr "Volumen" #: jack_mixer/channel.py:1083 jack_mixer/channel.py:1094 #: jack_mixer/channel.py:1105 jack_mixer/channel.py:1130 msgid "Learn" msgstr "Aprender" #: jack_mixer/channel.py:1087 msgid "_Balance" msgstr "_Balance" #: jack_mixer/channel.py:1091 msgid "Balance" msgstr "Balance" #: jack_mixer/channel.py:1098 msgid "M_ute" msgstr "M_ute" #: jack_mixer/channel.py:1102 msgid "Mute" msgstr "Mute" #: jack_mixer/channel.py:1112 msgid "_Direct Out(s)" msgstr "Sali_da(s) Directa(s)" #: jack_mixer/channel.py:1117 msgid "Add direct post-fader output(s) for channel." msgstr "Añadir salida(s) directas post-fader para canal." #: jack_mixer/channel.py:1123 msgid "S_olo" msgstr "S_olo" #: jack_mixer/channel.py:1127 msgid "Solo" msgstr "Solo" #: jack_mixer/channel.py:1164 msgid "Please move the MIDI control you want to use for this function." msgstr "" "Por favor mueve el control MIDI que quieres utilizar para esta función." #: jack_mixer/channel.py:1167 msgid "This window will close in 5 seconds." msgstr "Esta ventana se cerrará en 5 segundos." #: jack_mixer/channel.py:1173 #, python-brace-format msgid "This window will close in {seconds} seconds." msgstr "Esta ventana se cerrará en {seconds} segundos." #: jack_mixer/channel.py:1254 msgid "Value" msgstr "Valor" #: jack_mixer/channel.py:1255 msgid "-_Inf" msgstr "-_Inf" #: jack_mixer/channel.py:1256 msgid "_0dB" msgstr "_0dB" #: jack_mixer/channel.py:1263 msgid "New Input Channel" msgstr "Nuevo Canal Entrada" #: jack_mixer/channel.py:1296 msgid "_Color" msgstr "_Color" #: jack_mixer/channel.py:1305 msgid "Input Channels" msgstr "Canales Entrada" #: jack_mixer/channel.py:1307 msgid "_Display solo buttons" msgstr "_Mostrar botones de solo" #: jack_mixer/channel.py:1329 msgid "New Output Channel" msgstr "Nuevo Canal Salida" #: jack_mixer/channel.py:1397 msgid "Mute output channel send" msgstr "Silenciar envío canal salida" #: jack_mixer/channel.py:1403 msgid "Solo output send" msgstr "Solo envío salida" #: jack_mixer/channel.py:1407 msgid "P" msgstr "P" #: jack_mixer/channel.py:1409 msgid "Pre (on) / Post (off) fader send" msgstr "Envío fader Pre (on) / Post (off)" #: jack_mixer/gui.py:47 msgid "Use system setting" msgstr "Utilizar configuración del sistema" #: jack_mixer/gui.py:70 msgid "Cannot load PyXDG. " msgstr "No se pudo cargar PyXDG. " #: jack_mixer/gui.py:71 msgid "Your preferences will not be preserved across jack_mixer invocations." msgstr "" "Tus preferencias no se conservarán en diferentes invocaciones de jack_mixer." #: jack_mixer/gui.py:173 #, python-format msgid "Ignoring default_meter_scale setting, because '%s' scale is not known." msgstr "" "Ignorando la configuración default_meter_scale, porque la escala '%s' no es " "conocida." #: jack_mixer/gui.py:184 #, python-format msgid "Ignoring default_slider_scale setting, because '%s' scale is not known." msgstr "" "Ignorando la configuración default_slider_scale, porque la escala '%s' no es " "conocida." #: jack_mixer/preferences.py:30 msgid "Preferences" msgstr "Preferencias" #: jack_mixer/preferences.py:53 msgid "" "Set the path where mixer project files are saved and loaded from by default" msgstr "" #: jack_mixer/preferences.py:59 jack_mixer/preferences.py:71 msgid "Default Project Path" msgstr "Ruta Predeterminada de Proyecto" #: jack_mixer/preferences.py:76 msgid "Set the interface language and localisation" msgstr "" #: jack_mixer/preferences.py:80 msgid "Language:" msgstr "Idioma:" #: jack_mixer/preferences.py:83 msgid "Confirm quit" msgstr "Confirmar salida" #: jack_mixer/preferences.py:85 msgid "Always ask for confirmation before quitting the application" msgstr "Siempre pide confirmación antes de salir de la aplicación" #: jack_mixer/preferences.py:91 msgid "Use custom widgets" msgstr "Utilizar widgets personalizados" #: jack_mixer/preferences.py:93 msgid "Use widgets with custom design for the channel sliders" msgstr "" #: jack_mixer/preferences.py:99 msgid "Draw the volume meters with the selected solid color" msgstr "" #: jack_mixer/preferences.py:100 msgid "Use custom vumeter color" msgstr "Utilizar color personalizado de vúmetros" #: jack_mixer/preferences.py:113 msgid "Custom color:" msgstr "Color personalizado:" #: jack_mixer/preferences.py:121 msgid "Reset the peak meters after the specified time" msgstr "Auto resetear medidores de pico tras tiempo especificado" #: jack_mixer/preferences.py:122 msgid "Auto reset peak meter" msgstr "Auto resetear medidor de pico" #: jack_mixer/preferences.py:139 msgid "Time (s):" msgstr "Tiempo (s):" #: jack_mixer/preferences.py:149 msgid "" "Update the volume level meters with the specified interval in milliseconds" msgstr "" #: jack_mixer/preferences.py:152 msgid "Meter Refresh Period (ms):" msgstr "Periodo Refresco Medidor (ms):" #: jack_mixer/preferences.py:158 msgid "Interface" msgstr "Interfaz" #: jack_mixer/preferences.py:164 msgid "Set the scale for all volume meters" msgstr "" #: jack_mixer/preferences.py:165 msgid "Meter scale:" msgstr "Escala medidor:" #: jack_mixer/preferences.py:172 msgid "Set the scale for all volume sliders" msgstr "" #: jack_mixer/preferences.py:173 msgid "Slider scale:" msgstr "Escala potenciómetro:" #: jack_mixer/preferences.py:180 msgid "Scales" msgstr "Escalas" #: jack_mixer/preferences.py:187 msgid "" "Set how channel volume and balance are controlled via MIDI:\n" "\n" "- Jump To Value: channel volume or balance is set immediately to received " "controller value\n" "- Pick Up: control changes are ignored until a controller value near the " "current value is received\n" msgstr "" #: jack_mixer/preferences.py:191 msgid "Control Behavior:" msgstr "Controlar Comportamiento:" #: jack_mixer/preferences.py:198 msgid "MIDI" msgstr "MIDI" #: jack_mixer/preferences.py:309 msgid "You need to restart the application for this setting to take effect." msgstr "" "Debes reiniciar la aplicación para que esta configuración surta efecto." #: jack_mixer/scale.py:88 msgid "" "IEC 60268-18 Peak programme level meters - Digital audio peak level meter" msgstr "" "IEC 60268-18 Medidores de pico nivel programa - Medidor nivel pico de audio " "digital" #: jack_mixer/scale.py:115 msgid "" "IEC 60268-18 Peak programme level meters - Digital audio peak level meter, " "fewer marks" msgstr "" "IEC 60268-18 Medidores de pico nivel programa - Medidor nivel pico de audio " "digital, menos marcas" #: jack_mixer/scale.py:135 msgid "Linear scale with range from -70 to 0 dBFS" msgstr "Escala lineal con rango de -70 a 0 dBFS" #: jack_mixer/scale.py:156 msgid "Linear scale with range from -30 to +30 dBFS" msgstr "Escala lineal con rango de -30 a +30 dBFS" #: jack_mixer/scale.py:167 msgid "K20 scale" msgstr "Escala K20" #: jack_mixer/scale.py:207 msgid "K14 scale" msgstr "Escala K14" #: jack_mixer/serialization_xml.py:58 #, python-brace-format msgid "Document type '{type}' not supported." msgstr "Documento del tipo '{type}' no soportado." #: jack_mixer/slider.py:260 msgid "Center" msgstr "Centro" #: jack_mixer/slider.py:263 #, python-brace-format msgid "Left: {left} / Right: {right}" msgstr "Izquierda: {left} / Derecha: {right}" #: /usr/lib/python3.9/argparse.py:296 msgid "usage: " msgstr "utilización: " #: /usr/lib/python3.9/argparse.py:856 msgid ".__call__() not defined" msgstr ".__call__() no definido" #: /usr/lib/python3.9/argparse.py:1199 #, python-format msgid "unknown parser %(parser_name)r (choices: %(choices)s)" msgstr "unknown parser %(parser_name)r (opciones: %(choices)s)" #: /usr/lib/python3.9/argparse.py:1259 #, python-format msgid "argument \"-\" with mode %r" msgstr "argumento \"-\" con modo %r" #: /usr/lib/python3.9/argparse.py:1268 #, python-format msgid "can't open '%(filename)s': %(error)s" msgstr "no se puede abrir '%(filename)s': %(error)s" #: /usr/lib/python3.9/argparse.py:1477 #, python-format msgid "cannot merge actions - two groups are named %r" msgstr "no se pueden fusionar acciones - dos grupos tienen el nombre %r" #: /usr/lib/python3.9/argparse.py:1515 msgid "'required' is an invalid argument for positionals" msgstr "'required' es un argumento no válido para posicionales" #: /usr/lib/python3.9/argparse.py:1537 #, python-format msgid "" "invalid option string %(option)r: must start with a character " "%(prefix_chars)r" msgstr "" "cadena de opciones no válida %(option)r: debe comenzar con un carácter " "%(prefix_chars)r" #: /usr/lib/python3.9/argparse.py:1555 #, python-format msgid "dest= is required for options like %r" msgstr "dest= se require para opciones como %r" #: /usr/lib/python3.9/argparse.py:1572 #, python-format msgid "invalid conflict_resolution value: %r" msgstr "valor de conflict_resolution no válido: %r" #: /usr/lib/python3.9/argparse.py:1590 #, python-format msgid "conflicting option string: %s" msgid_plural "conflicting option strings: %s" msgstr[0] "conflicto de cadena de opciones: %s" msgstr[1] "" #: /usr/lib/python3.9/argparse.py:1656 msgid "mutually exclusive arguments must be optional" msgstr "los argumentos recíprocamente excluyentes deben ser opcionales" #: /usr/lib/python3.9/argparse.py:1723 msgid "positional arguments" msgstr "argumentos posicionales" #: /usr/lib/python3.9/argparse.py:1724 msgid "optional arguments" msgstr "argumentos opcionales" #: /usr/lib/python3.9/argparse.py:1739 msgid "show this help message and exit" msgstr "mostrar este mensaje de ayuda y salir" #: /usr/lib/python3.9/argparse.py:1770 msgid "cannot have multiple subparser arguments" msgstr "no se pueden tener múltiples argumentos de subanalizadores" #: /usr/lib/python3.9/argparse.py:1822 /usr/lib/python3.9/argparse.py:2333 #, python-format msgid "unrecognized arguments: %s" msgstr "argumentos no reconocidos: %s" #: /usr/lib/python3.9/argparse.py:1923 #, python-format msgid "not allowed with argument %s" msgstr "no se permite con argumento %s" #: /usr/lib/python3.9/argparse.py:1969 /usr/lib/python3.9/argparse.py:1983 #, python-format msgid "ignored explicit argument %r" msgstr "argumento explícito %r ignorado" #: /usr/lib/python3.9/argparse.py:2090 #, python-format msgid "the following arguments are required: %s" msgstr "se requieren los siguientes argumentos: %s" #: /usr/lib/python3.9/argparse.py:2105 #, python-format msgid "one of the arguments %s is required" msgstr "se requiere uno de los argumentos %s" #: /usr/lib/python3.9/argparse.py:2148 msgid "expected one argument" msgstr "se espera un argumento" #: /usr/lib/python3.9/argparse.py:2149 msgid "expected at most one argument" msgstr "se espera un argumento máximo" #: /usr/lib/python3.9/argparse.py:2150 msgid "expected at least one argument" msgstr "se espera un argumento mínimo" #: /usr/lib/python3.9/argparse.py:2154 #, python-format msgid "expected %s argument" msgid_plural "expected %s arguments" msgstr[0] "se espera argumento %s" msgstr[1] "expected %s arguments" #: /usr/lib/python3.9/argparse.py:2212 #, python-format msgid "ambiguous option: %(option)s could match %(matches)s" msgstr "opción ambigua: %(option)s podría(n) corresponder a %(matches)s" #: /usr/lib/python3.9/argparse.py:2276 #, python-format msgid "unexpected option string: %s" msgstr "cadena de opciones no esperada: %s" #: /usr/lib/python3.9/argparse.py:2473 #, python-format msgid "%r is not callable" msgstr "%r no es invocable" #: /usr/lib/python3.9/argparse.py:2490 #, python-format msgid "invalid %(type)s value: %(value)r" msgstr "valor %(type)s no válido: %(value)r" #: /usr/lib/python3.9/argparse.py:2501 #, python-format msgid "invalid choice: %(value)r (choose from %(choices)s)" msgstr "elección no válida: %(value)r (elegir entre %(choices)s)" #: /usr/lib/python3.9/argparse.py:2577 #, python-format msgid "%(prog)s: error: %(message)s\n" msgstr "%(prog)s: error: %(message)s\n" #: src/jack_mix_box.c:50 msgid "" "Usage: jack_mix_box [-n ] [-p] [-s] [-v ] MIDI_CC...\n" "\n" "-h|--help print this help message\n" "-n|--name set JACK client name\n" "-p|--pickup enable MIDI pickup mode (default: jump-to-value)\n" "-s|--stereo make all input channels stereo with left+right input\n" "-v|--volume initial volume gain in dBFS (default 0.0, i.e. unity gain)\n" "\n" "Each positional argument is interpreted as a MIDI Control Change number and\n" "adds a mixer channel with one (mono) or left+right (stereo) inputs, whose\n" "volume can be controlled via the given MIDI Control Change.\n" "\n" "Send SIGUSR1 to the process to have the current volumes reported per input\n" "channel.\n" "\n" msgstr "" "Utilización: jack_mix_box [-n ] [-p] [-s] [-v ] MIDI_CC...\n" "\n" "-h|--help mostrar este mensaje de ayuda\n" "-n|--name establecer nombre cliente JACK\n" "-p|--pickup habilitar modo enganche MIDI (predeterminado: saltar-al-valor)\n" "-s|--stereo hacer todos los canales de entrada estéreo con entrada izquierda" "+derecha\n" "-v|--volume ganancia inicial de volumen dBFS (predeterminado 0.0, esto es, " "ganancia de unidad)\n" "\n" "Cada argumento posicional se interpreta como un número de MIDI Control " "Change y\n" "añade un canal de mezclador con una entrada (mono) o entradas izquierda" "+derecha (estéreo), cuyo\n" "volumen se puede controlar a través del MIDI Control Change dado.\n" "\n" "Envía SIGUSR1 al proceso para obtener los volúmenes actuales para cada canal " "de\n" "entrada.\n" "\n" #: src/jack_mix_box.c:134 #, c-format msgid "Unknown argument, aborting.\n" msgstr "Argumento desconocido, abortando.\n" #: src/jack_mix_box.c:140 msgid "You must specify at least one input channel.\n" msgstr "Debes especificar al menos un canal de entrada.\n" #: src/jack_mix_box.c:176 #, c-format msgid "Failed to add channel %d, aborting.\n" msgstr "Error al añadir el canal %d, abortando.\n" #. JACK_MIXER_NO_ERROR #: src/jack_mixer.c:222 msgid "No error.\n" msgstr "No error.\n" #. JACK_MIXER_ERROR_JACK_CLIENT_CREATE #: src/jack_mixer.c:224 msgid "" "Could not create JACK client.\n" "Please make sure JACK daemon is running.\n" msgstr "" "No se pudo crear el cliente JACK.\n" "Por favor asegúrate de que el JACK se está ejecutando.\n" #. JACK_MIXER_ERROR_JACK_MIDI_IN_CREATE #: src/jack_mixer.c:226 msgid "Could not create JACK MIDI in port.\n" msgstr "No se pudo crear el puerto de entrada JACK MIDI\n" #. JACK_MIXER_ERROR_JACK_MIDI_OUT_CREATE #: src/jack_mixer.c:228 msgid "Could not create JACK MIDI out port.\n" msgstr "No se pudo crear el puerto de salida JACK MIDI\n" #. JACK_MIXER_ERROR_JACK_SET_PROCESS_CALLBACK #: src/jack_mixer.c:230 msgid "Could not set JACK process callback.\n" msgstr "No se pudo establecer callback de proceso de JACK.\n" #. JACK_MIXER_ERROR_JACK_SET_BUFFER_SIZE_CALLBACK #: src/jack_mixer.c:232 msgid "Could not set JACK buffer size callback.\n" msgstr "No se pudo establecer callback del tamaño de buffer de JACK.\n" #. JACK_MIXER_ERROR_JACK_ACTIVATE #: src/jack_mixer.c:234 msgid "Could not activate JACK client.\n" msgstr "No se pudo activar cliente de JACK.\n" #. JACK_MIXER_ERROR_CHANNEL_MALLOC #: src/jack_mixer.c:236 msgid "Could not allocate memory for channel.\n" msgstr "No se pudo asignar memoria para el canal.\n" #. JACK_MIXER_ERROR_CHANNEL_NAME_MALLOC #: src/jack_mixer.c:238 msgid "Could not allocate memory for channel name.\n" msgstr "No se pudo asignar memoria para el canal nombre.\n" #. JACK_MIXER_ERROR_PORT_REGISTER #: src/jack_mixer.c:240 msgid "Could not register JACK port for channel.\n" msgstr "No se pudo registrar puerto JACK para el canal.\n" #. JACK_MIXER_ERROR_PORT_REGISTER_LEFT #: src/jack_mixer.c:242 msgid "Could not register JACK port for left channel.\n" msgstr "No se pudo registrar puerto JACK para el canal izquierdo.\n" #. JACK_MIXER_ERROR_PORT_REGISTER_RIGHT #: src/jack_mixer.c:244 msgid "Could not register JACK port for right channel.\n" msgstr "No se pudo registrar puerto JACK para el canal derecho.\n" #. JACK_MIXER_ERROR_JACK_RENAME_PORT #: src/jack_mixer.c:246 msgid "Could not rename JACK port for channel.\n" msgstr "No se pudo renombrar puerto JACK para el canal.\n" #. JACK_MIXER_ERROR_JACK_RENAME_PORT_LEFT #: src/jack_mixer.c:248 msgid "Could not rename JACK port for left channel.\n" msgstr "No se pudo renombrar puerto JACK para el canal izquierdo.\n" #. JACK_MIXER_ERROR_JACK_RENAME_PORT_LEFT #: src/jack_mixer.c:250 msgid "Could not rename JACK port for right channel.\n" msgstr "No se pudo renombrar puerto JACK para el canal derecho.\n" #. JACK_MIXER_ERROR_PORT_NAME_MALLOC #: src/jack_mixer.c:252 msgid "Could not allocate memory for port name.\n" msgstr "No se pudo asignar memoria para el puerto nombre.\n" #. JACK_MIXER_ERROR_INVALID_CC #: src/jack_mixer.c:254 msgid "Control Change number out of range.\n" msgstr "Número Control Change fuera de rango.\n" #. JACK_MIXER_ERROR_NO_FREE_CC #: src/jack_mixer.c:256 msgid "No free Control Change number.\n" msgstr "Ningún número Control Change libre.\n" #: src/jack_mixer.c:811 #, c-format msgid "%s: volume is %f dbFS for mixer channel: %s\n" msgstr "%s: volumen es %f dbFS para canal del mezclador: %s\n" jack_mixer-release-17/data/locale/jack_mixer-fr.po000066400000000000000000000537771413224161500223100ustar00rootroot00000000000000# jack_mixer i18n message catalog template # # This file is distributed under the same license as the jack_mixer package. # # Copyright (C) 2021 Olivier (trebmuh/olinuxx) Humbert # msgid "" msgstr "" "Project-Id-Version: jack_mixer 16\n" "Report-Msgid-Bugs-To: https://github.com/jack-mixer/jack_mixer/issues\n" "POT-Creation-Date: 2021-04-14 17:19+0200\n" "PO-Revision-Date: 2021-04-08 13:30+0100\n" "Last-Translator: 2021 Olivier (trebmuh/olinuxx) Humbert \n" "Language-Team: French\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: jack_mixer/app.py:48 msgid "" "A multi-channel audio mixer application for the JACK Audio Connection Kit." msgstr "" "Une application de mixage audio multi-canal pour le kit de connexion audio " "JACK." #: jack_mixer/app.py:49 msgid "" "jack_mixer is free software; you can redistribute it and/or modify it\n" "under the terms of the GNU General Public License as published by the\n" "Free Software Foundation; either version 2 of the License, or (at your\n" "option) any later version.\n" "\n" "jack_mixer is distributed in the hope that it will be useful, but\n" "WITHOUT ANY WARRANTY; without even the implied warranty of\n" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" "General Public License for more details.\n" "\n" "You should have received a copy of the GNU General Public License along\n" "with jack_mixer; if not, write to the Free Software Foundation, Inc., 51\n" "Franklin Street, Fifth Floor, Boston, MA 02110-130159 USA\n" msgstr "" #: jack_mixer/app.py:169 msgid "jack_mixer XML files" msgstr "fichiers XML jack_mixer" #: jack_mixer/app.py:182 msgid "_Recent Projects" msgstr "Projets _récents" #: jack_mixer/app.py:218 msgid "_Mixer" msgstr "_Mixeur" #: jack_mixer/app.py:220 msgid "_Edit" msgstr "Modifi_er" #: jack_mixer/app.py:222 msgid "_Help" msgstr "_Aide" #: jack_mixer/app.py:230 msgid "New _Input Channel" msgstr "_Nouveau canal d'entrée" #: jack_mixer/app.py:234 msgid "New Output _Channel" msgstr "Nouveau _canal de de sortie" #: jack_mixer/app.py:241 msgid "_Open..." msgstr "_Ouvrir" #: jack_mixer/app.py:247 msgid "_Save" msgstr "_Sauvegarder" #: jack_mixer/app.py:251 msgid "Save _As..." msgstr "S_auvegarder sous..." #: jack_mixer/app.py:256 msgid "_Hide" msgstr "Cac_her" #: jack_mixer/app.py:258 msgid "_Quit" msgstr "_Quitter" #: jack_mixer/app.py:265 msgid "_Edit Input Channel" msgstr "Modifier le canal d'_entrée" #: jack_mixer/app.py:272 msgid "E_dit Output Channel" msgstr "Modifier le canal _de sortie" #: jack_mixer/app.py:279 msgid "_Remove Input Channel" msgstr "Supp_rimer le canal d'entrée" #: jack_mixer/app.py:286 msgid "Re_move Output Channel" msgstr "Suppri_mer le canal de sortie" #: jack_mixer/app.py:294 msgid "Shrink Channels" msgstr "Rétracter les canaux" #: jack_mixer/app.py:297 msgid "Expand Channels" msgstr "Étendre les canaux" #: jack_mixer/app.py:301 msgid "_Clear" msgstr "_Nettoyer" #: jack_mixer/app.py:306 msgid "_Preferences" msgstr "_Préférences" #: jack_mixer/app.py:313 msgid "_About" msgstr "À _propos" #: jack_mixer/app.py:363 msgid "Input channel creation failed." msgstr "Impossible de créer un canal d'entrée." #: jack_mixer/app.py:425 msgid "Output channel creation failed." msgstr "Impossible de créer un canal de sortie." #: jack_mixer/app.py:484 jack_mixer/app.py:573 jack_mixer/app.py:1158 #, python-brace-format msgid "Error loading project file '{filename}': {msg}" msgstr "Erreur lors du chargement du fichier projet '{filename}': {msg}" #: jack_mixer/app.py:558 msgid "XML files" msgstr "Fichiers XML" #: jack_mixer/app.py:562 msgid "All files" msgstr "Tous les fichiers" #: jack_mixer/app.py:583 msgid "Open project" msgstr "Ouvrir un projet" #: jack_mixer/app.py:631 jack_mixer/app.py:674 #, python-brace-format msgid "Error saving project file '{filename}': {msg}" msgstr "Erreur lors de la sauvegarde du fichier projet '{filename}': {msg}" #: jack_mixer/app.py:638 msgid "Save project" msgstr "Sauvegarder le projet" #: jack_mixer/app.py:691 msgid "Quit application?" msgstr "Quitter l'application ?" #: jack_mixer/app.py:694 msgid "" "All jack_mixer ports will be closed and connections lost,\n" "stopping all sound going through jack_mixer.\n" "\n" "Are you sure?" msgstr "" "Tous les ports de jack_mixer seront fermés et les connexions perdues,\n" "ce qui arrêtera tout son passant par jack_mixer.\n" "\n" "Êtes-vous sûr ?" # Don't translate this unless you want default channel names to be localized #: jack_mixer/app.py:759 msgid "Input" msgstr "Entrée" # Don't translate this unless you want default channel names to be localized #: jack_mixer/app.py:762 msgid "Output" msgstr "Sortie" #: jack_mixer/app.py:890 msgid "Are you sure you want to clear all channels?" msgstr "Êtes-vous sûr de vouloir nettoyer tous les canaux ?" #: jack_mixer/app.py:1122 msgid "FILE" msgstr "FICHIER" #: jack_mixer/app.py:1123 msgid "load mixer project configuration from FILE" msgstr "charger la configuration du projet de mixeur à partir du FICHIER" #: jack_mixer/app.py:1130 msgid "enable debug logging messages" msgstr "activer les messages de journalisation de débogage" #: jack_mixer/app.py:1134 msgid "NAME" msgstr "NOM" #: jack_mixer/app.py:1137 #, python-format msgid "set JACK client name (default: %(default)s)" msgstr "paramètre le nom de client JACK (défaut : %(default)s)" #: jack_mixer/app.py:1148 msgid "" "Mixer creation failed:\n" "\n" "{}" msgstr "" "Impossible de créer le mixeur :\n" "\n" "{}" #: jack_mixer/channel.py:113 jack_mixer/channel.py:1329 msgid "M" msgstr "" #: jack_mixer/channel.py:122 msgid "MON" msgstr "" #: jack_mixer/channel.py:569 msgid "S" msgstr "" #: jack_mixer/channel.py:580 msgid "Cannot create a channel" msgstr "Impossible de créer un canal" #: jack_mixer/channel.py:795 msgid "Cannot create an output channel" msgstr "Impossible de créer un canal de sortie" #: jack_mixer/channel.py:940 #, python-brace-format msgid "Channel '{name}' Properties" msgstr "Propriétés du canal '{name}'" #: jack_mixer/channel.py:981 msgid "Properties" msgstr "Propriétés" #: jack_mixer/channel.py:986 msgid "_Name" msgstr "_Nom" #: jack_mixer/channel.py:995 msgid "Mode" msgstr "Mode" #: jack_mixer/channel.py:996 msgid "_Mono" msgstr "_Mono" #: jack_mixer/channel.py:997 msgid "_Stereo" msgstr "_Stéréo" #: jack_mixer/channel.py:1002 msgid "MIDI Control Changes" msgstr "Control Changes MIDI" #: jack_mixer/channel.py:1008 #, python-brace-format msgid "" "{param} MIDI Control Change number (0-127, set to -1 to assign next free CC " "#)" msgstr "" "Numéro de Control Change MIDI pour {param} (0-127, mettre à -1 pour assigner " "le prochain CC libre #)" #: jack_mixer/channel.py:1010 msgid "_Volume" msgstr "" #: jack_mixer/channel.py:1014 msgid "Volume" msgstr "" #: jack_mixer/channel.py:1017 jack_mixer/channel.py:1028 #: jack_mixer/channel.py:1039 jack_mixer/channel.py:1064 msgid "Learn" msgstr "Apprentissage" #: jack_mixer/channel.py:1021 msgid "_Balance" msgstr "" #: jack_mixer/channel.py:1025 msgid "Balance" msgstr "" #: jack_mixer/channel.py:1032 msgid "M_ute" msgstr "M_uet" #: jack_mixer/channel.py:1036 msgid "Mute" msgstr "Muet" #: jack_mixer/channel.py:1046 msgid "_Direct Out(s)" msgstr "Sortie(s) _directes" #: jack_mixer/channel.py:1051 msgid "Add direct post-fader output(s) for channel." msgstr "Ajouter une ou plusieurs sorties directes post-fader pour le canal." #: jack_mixer/channel.py:1057 msgid "S_olo" msgstr "" #: jack_mixer/channel.py:1061 msgid "Solo" msgstr "" #: jack_mixer/channel.py:1098 msgid "Please move the MIDI control you want to use for this function." msgstr "" "Veuillez déplacer le contrôle MIDI que vous souhaitez utiliser pour cette " "fonction." #: jack_mixer/channel.py:1101 msgid "This window will close in 5 seconds." msgstr "Cette fenêtre se fermera dans 5 secondes." #: jack_mixer/channel.py:1107 #, python-brace-format msgid "This window will close in {seconds} seconds." msgstr "Cette fenêtre se fermera dans {seconds} secondes." #: jack_mixer/channel.py:1188 msgid "Value" msgstr "Valeur" #: jack_mixer/channel.py:1189 msgid "-_Inf" msgstr "" #: jack_mixer/channel.py:1190 msgid "_0dB" msgstr "" #: jack_mixer/channel.py:1197 msgid "New Input Channel" msgstr "Nouveau canal d'entrée" #: jack_mixer/channel.py:1230 msgid "_Color" msgstr "_Couleur" #: jack_mixer/channel.py:1239 msgid "Input Channels" msgstr "Canaux d'entrée" #: jack_mixer/channel.py:1241 msgid "_Display solo buttons" msgstr "Affichage _des boutons solo" #: jack_mixer/channel.py:1263 msgid "New Output Channel" msgstr "Nouveau canal de sortie" #: jack_mixer/channel.py:1331 msgid "Mute output channel send" msgstr "Silencer l'envoi du canal de sortie" #: jack_mixer/channel.py:1337 msgid "Solo output send" msgstr "Envoi de la sortie solo" #: jack_mixer/channel.py:1341 msgid "P" msgstr "" #: jack_mixer/channel.py:1343 msgid "Pre (on) / Post (off) fader send" msgstr "Envoi pré (on) / post (off) fader" #: jack_mixer/gui.py:47 msgid "Use system setting" msgstr "Utiliser le paramètre système" #: jack_mixer/gui.py:69 msgid "Cannot load PyXDG. " msgstr "Impossible de charger PyXDG." #: jack_mixer/gui.py:70 msgid "Your preferences will not be preserved across jack_mixer invocations." msgstr "" "Vos préférences ne seront pas préservées entre les invocations de jack_mixer." #: jack_mixer/gui.py:152 #, python-format msgid "Ignoring default_meter_scale setting, because '%s' scale is not known." msgstr "" "Ignorer le paramètre default_meter_scale, car l'échelle '%s' est inconnue." #: jack_mixer/gui.py:163 #, python-format msgid "Ignoring default_slider_scale setting, because '%s' scale is not known." msgstr "" "Ignorer le paramètre default_slider_scale, car l'échelle '%s' est inconnue." #: jack_mixer/preferences.py:30 msgid "Preferences" msgstr "Préférences" #: jack_mixer/preferences.py:56 jack_mixer/preferences.py:68 msgid "Default Project Path" msgstr "Chemin de projet par défaut" #: jack_mixer/preferences.py:75 msgid "Language:" msgstr "Langue : " #: jack_mixer/preferences.py:79 msgid "Confirm quit" msgstr "Confirmer la sortie" #: jack_mixer/preferences.py:81 msgid "Always ask for confirmation before quitting the application" msgstr "Toujours demander une confirmation avant de quitter l'application" #: jack_mixer/preferences.py:87 msgid "Use custom widgets" msgstr "Utiliser les widgets personnalisés" #: jack_mixer/preferences.py:92 msgid "Use custom vumeter color" msgstr "Utiliser les couleurs personnalisées de VU-mètre" #: jack_mixer/preferences.py:103 msgid "Custom color:" msgstr "Couleur personnalisée : " #: jack_mixer/preferences.py:111 msgid "Interface" msgstr "" #: jack_mixer/preferences.py:117 msgid "Meter scale" msgstr "Échelle de mesure" #: jack_mixer/preferences.py:121 msgid "Slider scale" msgstr "Échelle du curseur" #: jack_mixer/preferences.py:125 msgid "Scales" msgstr "Échelles" #: jack_mixer/preferences.py:131 msgid "Control Behavior" msgstr "Contrôle du comportement" #: jack_mixer/preferences.py:135 msgid "MIDI" msgstr "" #: jack_mixer/preferences.py:221 msgid "You need to restart the application for this setting to take effect." msgstr "Vous devez redémarrer l'application pour que ce paramètre prenne effet." #: jack_mixer/scale.py:88 msgid "" "IEC 60268-18 Peak programme level meters - Digital audio peak level meter" msgstr "" "IEC 60268-18 Indicateurs de niveau de programme de crête - Indicateur de " "niveau de crête audio numérique" #: jack_mixer/scale.py:115 msgid "" "IEC 60268-18 Peak programme level meters - Digital audio peak level meter, " "fewer marks" msgstr "" "IEC 60268-18 Indicateurs de niveau de programme de crête - Indicateur de " "niveau de crête audio numérique, moins de marques" #: jack_mixer/scale.py:135 msgid "Linear scale with range from -70 to 0 dBFS" msgstr "Échelle linéaire avec une gamme de -70 à 0 dBFS" #: jack_mixer/scale.py:156 msgid "Linear scale with range from -30 to +30 dBFS" msgstr "Échelle linéaire avec une gamme de -30 to +30 dBFS" #: jack_mixer/scale.py:167 msgid "K20 scale" msgstr "Échelle K20" #: jack_mixer/scale.py:207 msgid "K14 scale" msgstr "Échelle K14" #: jack_mixer/serialization_xml.py:58 #, python-brace-format msgid "Document type '{type}' not supported." msgstr "Type de document '{type}' non pris en charge." #: jack_mixer/slider.py:249 msgid "Center" msgstr "Centre" #: jack_mixer/slider.py:252 #, python-brace-format msgid "Left: {left} / Right: {right}" msgstr "Gauche : {left} / droite : {right}" #: /usr/lib/python3.9/argparse.py:296 msgid "usage: " msgstr "utilisation : " #: /usr/lib/python3.9/argparse.py:854 msgid ".__call__() not defined" msgstr ".__call__() non défini" #: /usr/lib/python3.9/argparse.py:1197 #, python-format msgid "unknown parser %(parser_name)r (choices: %(choices)s)" msgstr "analyseur inconnu %(parser_name)r (choix : %(choices)s)" #: /usr/lib/python3.9/argparse.py:1257 #, python-format msgid "argument \"-\" with mode %r" msgstr "argument \"-\" avec mode %r" #: /usr/lib/python3.9/argparse.py:1266 #, python-format msgid "can't open '%(filename)s': %(error)s" msgstr "impossible d'ouvrir '%(filename)s': %(error)s" #: /usr/lib/python3.9/argparse.py:1475 #, python-format msgid "cannot merge actions - two groups are named %r" msgstr "impossible de fusionner les actions - deux groupes sont nommés %r" #: /usr/lib/python3.9/argparse.py:1513 msgid "'required' is an invalid argument for positionals" msgstr "'required' est un argument invalide pour les positionnels" #: /usr/lib/python3.9/argparse.py:1535 #, python-format msgid "" "invalid option string %(option)r: must start with a character " "%(prefix_chars)r" msgstr "" "chaîne d'option invalide %(option)r : doit commencer par un caractère " "%(prefix_chars)r" #: /usr/lib/python3.9/argparse.py:1553 #, python-format msgid "dest= is required for options like %r" msgstr "dest= est nécessaire pour les options comme %r" #: /usr/lib/python3.9/argparse.py:1570 #, python-format msgid "invalid conflict_resolution value: %r" msgstr "valeur conflict_resolution invalide : %r" #: /usr/lib/python3.9/argparse.py:1588 #, python-format msgid "conflicting option string: %s" msgid_plural "conflicting option strings: %s" msgstr[0] "chaîne d'options contradictoires : %s" msgstr[1] "chaînes d'options contradictoires : %s" #: /usr/lib/python3.9/argparse.py:1654 msgid "mutually exclusive arguments must be optional" msgstr "les arguments mutuellement exclusifs doivent être facultatifs" #: /usr/lib/python3.9/argparse.py:1721 msgid "positional arguments" msgstr "arguments positionnels" #: /usr/lib/python3.9/argparse.py:1722 msgid "optional arguments" msgstr "arguments facultatifs" #: /usr/lib/python3.9/argparse.py:1737 msgid "show this help message and exit" msgstr "affiche ce message d'aide et quitte" #: /usr/lib/python3.9/argparse.py:1768 msgid "cannot have multiple subparser arguments" msgstr "impossible d'avoir plusieurs arguments de sous-analyse" #: /usr/lib/python3.9/argparse.py:1820 /usr/lib/python3.9/argparse.py:2331 #, python-format msgid "unrecognized arguments: %s" msgstr "arguments non-reconnus : %s" #: /usr/lib/python3.9/argparse.py:1921 #, python-format msgid "not allowed with argument %s" msgstr "non autorisé avec l'argument %s" #: /usr/lib/python3.9/argparse.py:1967 /usr/lib/python3.9/argparse.py:1981 #, python-format msgid "ignored explicit argument %r" msgstr "argument spécifique %r ignoré" #: /usr/lib/python3.9/argparse.py:2088 #, python-format msgid "the following arguments are required: %s" msgstr "les arguments suivants sont requis : %s" #: /usr/lib/python3.9/argparse.py:2103 #, python-format msgid "one of the arguments %s is required" msgstr "un des arguments %s est requis" #: /usr/lib/python3.9/argparse.py:2146 msgid "expected one argument" msgstr "attendu un argument" #: /usr/lib/python3.9/argparse.py:2147 msgid "expected at most one argument" msgstr "attendu au plus un argument" #: /usr/lib/python3.9/argparse.py:2148 msgid "expected at least one argument" msgstr "attendu au moins un argument" #: /usr/lib/python3.9/argparse.py:2152 #, python-format msgid "expected %s argument" msgid_plural "expected %s arguments" msgstr[0] "argument %s attendu" msgstr[1] "arguments %s attendus" #: /usr/lib/python3.9/argparse.py:2210 #, python-format msgid "ambiguous option: %(option)s could match %(matches)s" msgstr "option ambiguë : %(option)s pourrait correspondre à %(matches)s" #: /usr/lib/python3.9/argparse.py:2274 #, python-format msgid "unexpected option string: %s" msgstr "chaîne d'option inattendue : %s" #: /usr/lib/python3.9/argparse.py:2471 #, python-format msgid "%r is not callable" msgstr "%r n'est pas appelable" #: /usr/lib/python3.9/argparse.py:2488 #, python-format msgid "invalid %(type)s value: %(value)r" msgstr "valeur %(type)s invalide : %(value)r" #: /usr/lib/python3.9/argparse.py:2499 #, python-format msgid "invalid choice: %(value)r (choose from %(choices)s)" msgstr "choix invalide : %(value)r (choisir parmi %(choices)s)" #: /usr/lib/python3.9/argparse.py:2575 #, python-format msgid "%(prog)s: error: %(message)s\n" msgstr "%(prog)s: erreur : %(message)s\n" #: src/jack_mix_box.c:50 msgid "" "Usage: jack_mix_box [-n ] [-p] [-s] [-v ] MIDI_CC...\n" "\n" "-h|--help print this help message\n" "-n|--name set JACK client name\n" "-p|--pickup enable MIDI pickup mode (default: jump-to-value)\n" "-s|--stereo make all input channels stereo with left+right input\n" "-v|--volume initial volume gain in dBFS (default 0.0, i.e. unity gain)\n" "\n" "Each positional argument is interpreted as a MIDI Control Change number and\n" "adds a mixer channel with one (mono) or left+right (stereo) inputs, whose\n" "volume can be controlled via the given MIDI Control Change.\n" "\n" "Send SIGUSR1 to the process to have the current volumes reported per input\n" "channel.\n" "\n" msgstr "" "Utilisation : jack_mix_box [-n ] [-p] [-s] [-v ] MIDI_CC...\n" "\n" "-h|--help imprimer ce message d'aide\n" "-n|--name définir le nom du client JACK\n" "-p|--pickup active le mode pickup MIDI (par défaut : jump-to-value)\n" "-s|--stereo rendre tous les canaux d'entrée stéréo avec entrée gauche+droite\n" "-v|--volume gain de volume initial en dBFS (par défaut 0.0, c-à-d gain unitaire)\n" "\n" "Chaque argument positionnel est interprété comme un numéro de Control Change\n" "MIDI et ajoute une tranche de mix avec une entrée (mono) ou gauche+droite\n" "(stéréo), dont le volume peut être contrôlé via le Control Change MIDI donné.\n" "\n" "Envoyer SIGUSR1 au processus pour avoir les volumes courants rapportés par\n" "canal d'entrée.\n" "\n" #: src/jack_mix_box.c:134 #, c-format msgid "Unknown argument, aborting.\n" msgstr "Argument inconnu, abandon.\n" #: src/jack_mix_box.c:140 #, c-format msgid "You must specify at least one input channel.\n" msgstr "Vous devez spécifier au moins un canal d'entrée.\n" #: src/jack_mix_box.c:176 #, c-format msgid "Failed to add channel %d, aborting.\n" msgstr "Impossible d'ajouter un canal %d, abandon.\n" #. JACK_MIXER_NO_ERROR #: src/jack_mixer.c:212 msgid "No error.\n" msgstr "Pas d'erreur.\n" #. JACK_MIXER_ERROR_JACK_CLIENT_CREATE #: src/jack_mixer.c:214 msgid "" "Could not create JACK client.\n" "Please make sure JACK daemon is running.\n" msgstr "" "Impossible de créer le client JACK.\n" "Veuillez vous assurer que le démon JACK est en cours d'exécution.\n" #. JACK_MIXER_ERROR_JACK_MIDI_IN_CREATE #: src/jack_mixer.c:216 msgid "Could not create JACK MIDI in port.\n" msgstr "Impossible de créer une entrée JACK MIDI.\n" #. JACK_MIXER_ERROR_JACK_MIDI_OUT_CREATE #: src/jack_mixer.c:218 msgid "Could not create JACK MIDI out port.\n" msgstr "Impossible de créer une sortie JACK MIDI.\n" #. JACK_MIXER_ERROR_JACK_SET_PROCESS_CALLBACK #: src/jack_mixer.c:220 msgid "Could not set JACK process callback.\n" msgstr "Impossible de définir le rappel du processus JACK.\n" #. JACK_MIXER_ERROR_JACK_ACTIVATE #: src/jack_mixer.c:222 msgid "Could not activate JACK client.\n" msgstr "Impossible d'activer le client JACK.\n" #. JACK_MIXER_ERROR_CHANNEL_MALLOC #: src/jack_mixer.c:224 msgid "Could not allocate memory for channel.\n" msgstr "Impossible d'allouer de la mémoire pour le canal.\n" #. JACK_MIXER_ERROR_CHANNEL_NAME_MALLOC #: src/jack_mixer.c:226 msgid "Could not allocate memory for channel name.\n" msgstr "Impossible d'allouer de la mémoire pour le nom de canal.\n" #. JACK_MIXER_ERROR_PORT_REGISTER #: src/jack_mixer.c:228 msgid "Could not register JACK port for channel.\n" msgstr "Impossible d'enregistrer un port JACK pour le canal.\n" #. JACK_MIXER_ERROR_PORT_REGISTER_LEFT #: src/jack_mixer.c:230 msgid "Could not register JACK port for left channel.\n" msgstr "Impossible d'enregistrer un port JACK pour le canal de gauche.\n" #. JACK_MIXER_ERROR_PORT_REGISTER_RIGHT #: src/jack_mixer.c:232 msgid "Could not register JACK port for right channel.\n" msgstr "Impossible d'enregistrer un port JACK pour le canal de droite.\n" #. JACK_MIXER_ERROR_JACK_RENAME_PORT #: src/jack_mixer.c:234 msgid "Could not rename JACK port for channel.\n" msgstr "Impossible de renommer le port JACK pour le canal.\n" #. JACK_MIXER_ERROR_JACK_RENAME_PORT_LEFT #: src/jack_mixer.c:236 msgid "Could not rename JACK port for left channel.\n" msgstr "Impossible de renommer le port JACK pour le canal de gauche.\n" #. JACK_MIXER_ERROR_JACK_RENAME_PORT_LEFT #: src/jack_mixer.c:238 msgid "Could not rename JACK port for right channel.\n" msgstr "Impossible de renommer le port JACK pour le canal de droite.\n" #. JACK_MIXER_ERROR_PORT_NAME_MALLOC #: src/jack_mixer.c:240 msgid "Could not allocate memory for port name.\n" msgstr "Impossible d'allouer de la mémoire pour le nom du port.\n" #. JACK_MIXER_ERROR_INVALID_CC #: src/jack_mixer.c:242 msgid "Control Change number out of range.\n" msgstr "Numéro Control Change hors de l'intervalle.\n" #. JACK_MIXER_ERROR_NO_FREE_CC #: src/jack_mixer.c:244 msgid "No free Control Change number.\n" msgstr "Pas de Control Change libre.\n" #: src/jack_mixer.c:748 #, c-format msgid "%s: volume is %f dbFS for mixer channel: %s\n" msgstr "%s : le volume est de %f dbFS pour la tranche de mix : %s\n" jack_mixer-release-17/data/locale/jack_mixer.pot000066400000000000000000000425661413224161500220610ustar00rootroot00000000000000# jack_mixer i18n message catalog template # # This file is distributed under the same license as the jack_mixer package. # # Copyright (C) 2021 Christopher Arndt # msgid "" msgstr "" "Project-Id-Version: jack_mixer 17\n" "Report-Msgid-Bugs-To: https://github.com/jack-mixer/jack_mixer/issues\n" "POT-Creation-Date: 2021-10-14 12:58+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" #: jack_mixer/app.py:48 msgid "" "A multi-channel audio mixer application for the JACK Audio Connection Kit." msgstr "" #: jack_mixer/app.py:49 msgid "" "jack_mixer is free software; you can redistribute it and/or modify it\n" "under the terms of the GNU General Public License as published by the\n" "Free Software Foundation; either version 2 of the License, or (at your\n" "option) any later version.\n" "\n" "jack_mixer is distributed in the hope that it will be useful, but\n" "WITHOUT ANY WARRANTY; without even the implied warranty of\n" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n" "General Public License for more details.\n" "\n" "You should have received a copy of the GNU General Public License along\n" "with jack_mixer; if not, write to the Free Software Foundation, Inc., 51\n" "Franklin Street, Fifth Floor, Boston, MA 02110-130159 USA\n" msgstr "" #: jack_mixer/app.py:174 msgid "jack_mixer XML files" msgstr "" #: jack_mixer/app.py:187 msgid "_Recent Projects" msgstr "" #: jack_mixer/app.py:230 msgid "_Mixer" msgstr "" #: jack_mixer/app.py:232 msgid "_Edit" msgstr "" #: jack_mixer/app.py:234 msgid "_Help" msgstr "" #: jack_mixer/app.py:242 msgid "New _Input Channel" msgstr "" #: jack_mixer/app.py:246 msgid "New Output _Channel" msgstr "" #: jack_mixer/app.py:253 msgid "_Open..." msgstr "" #: jack_mixer/app.py:259 msgid "_Save" msgstr "" #: jack_mixer/app.py:263 msgid "Save _As..." msgstr "" #: jack_mixer/app.py:268 msgid "_Hide" msgstr "" #: jack_mixer/app.py:270 msgid "_Quit" msgstr "" #: jack_mixer/app.py:277 msgid "_Edit Input Channel" msgstr "" #: jack_mixer/app.py:284 msgid "E_dit Output Channel" msgstr "" #: jack_mixer/app.py:291 msgid "_Remove Input Channel" msgstr "" #: jack_mixer/app.py:298 msgid "Re_move Output Channel" msgstr "" #: jack_mixer/app.py:305 msgid "Shrink Channels" msgstr "" #: jack_mixer/app.py:309 msgid "Expand Channels" msgstr "" #: jack_mixer/app.py:322 msgid "_Clear" msgstr "" #: jack_mixer/app.py:327 msgid "_Preferences" msgstr "" #: jack_mixer/app.py:334 msgid "_About" msgstr "" #: jack_mixer/app.py:384 msgid "Input channel creation failed." msgstr "" #: jack_mixer/app.py:446 msgid "Output channel creation failed." msgstr "" #: jack_mixer/app.py:505 jack_mixer/app.py:594 jack_mixer/app.py:1199 #, python-brace-format msgid "Error loading project file '{filename}': {msg}" msgstr "" #: jack_mixer/app.py:579 msgid "XML files" msgstr "" #: jack_mixer/app.py:583 msgid "All files" msgstr "" #: jack_mixer/app.py:604 msgid "Open project" msgstr "" #: jack_mixer/app.py:652 jack_mixer/app.py:695 #, python-brace-format msgid "Error saving project file '{filename}': {msg}" msgstr "" #: jack_mixer/app.py:659 msgid "Save project" msgstr "" #: jack_mixer/app.py:712 msgid "Quit application?" msgstr "" #: jack_mixer/app.py:715 msgid "" "All jack_mixer ports will be closed and connections lost,\n" "stopping all sound going through jack_mixer.\n" "\n" "Are you sure?" msgstr "" #: jack_mixer/app.py:788 msgid "Input" msgstr "" #: jack_mixer/app.py:791 msgid "Output" msgstr "" #: jack_mixer/app.py:919 msgid "Are you sure you want to clear all channels?" msgstr "" #: jack_mixer/app.py:1163 msgid "FILE" msgstr "" #: jack_mixer/app.py:1164 msgid "load mixer project configuration from FILE" msgstr "" #: jack_mixer/app.py:1171 msgid "enable debug logging messages" msgstr "" #: jack_mixer/app.py:1175 msgid "NAME" msgstr "" #: jack_mixer/app.py:1178 #, python-format msgid "set JACK client name (default: %(default)s)" msgstr "" #: jack_mixer/app.py:1189 msgid "" "Mixer creation failed:\n" "\n" "{}" msgstr "" #: jack_mixer/channel.py:115 jack_mixer/channel.py:1395 msgid "M" msgstr "" #: jack_mixer/channel.py:124 msgid "MON" msgstr "" #: jack_mixer/channel.py:134 msgid "PRE" msgstr "" #: jack_mixer/channel.py:136 msgid "Pre-fader (on) / Post-fader (off) metering" msgstr "" #: jack_mixer/channel.py:630 msgid "S" msgstr "" #: jack_mixer/channel.py:641 msgid "Cannot create a channel" msgstr "" #: jack_mixer/channel.py:858 msgid "Cannot create an output channel" msgstr "" #: jack_mixer/channel.py:1006 #, python-brace-format msgid "Channel '{name}' Properties" msgstr "" #: jack_mixer/channel.py:1047 msgid "Properties" msgstr "" #: jack_mixer/channel.py:1052 msgid "_Name" msgstr "" #: jack_mixer/channel.py:1061 msgid "Mode" msgstr "" #: jack_mixer/channel.py:1062 msgid "_Mono" msgstr "" #: jack_mixer/channel.py:1063 msgid "_Stereo" msgstr "" #: jack_mixer/channel.py:1068 msgid "MIDI Control Changes" msgstr "" #: jack_mixer/channel.py:1074 #, python-brace-format msgid "" "{param} MIDI Control Change number (0-127, set to -1 to assign next free CC " "#)" msgstr "" #: jack_mixer/channel.py:1076 msgid "_Volume" msgstr "" #: jack_mixer/channel.py:1080 msgid "Volume" msgstr "" #: jack_mixer/channel.py:1083 jack_mixer/channel.py:1094 #: jack_mixer/channel.py:1105 jack_mixer/channel.py:1130 msgid "Learn" msgstr "" #: jack_mixer/channel.py:1087 msgid "_Balance" msgstr "" #: jack_mixer/channel.py:1091 msgid "Balance" msgstr "" #: jack_mixer/channel.py:1098 msgid "M_ute" msgstr "" #: jack_mixer/channel.py:1102 msgid "Mute" msgstr "" #: jack_mixer/channel.py:1112 msgid "_Direct Out(s)" msgstr "" #: jack_mixer/channel.py:1117 msgid "Add direct post-fader output(s) for channel." msgstr "" #: jack_mixer/channel.py:1123 msgid "S_olo" msgstr "" #: jack_mixer/channel.py:1127 msgid "Solo" msgstr "" #: jack_mixer/channel.py:1164 msgid "Please move the MIDI control you want to use for this function." msgstr "" #: jack_mixer/channel.py:1167 msgid "This window will close in 5 seconds." msgstr "" #: jack_mixer/channel.py:1173 #, python-brace-format msgid "This window will close in {seconds} seconds." msgstr "" #: jack_mixer/channel.py:1254 msgid "Value" msgstr "" #: jack_mixer/channel.py:1255 msgid "-_Inf" msgstr "" #: jack_mixer/channel.py:1256 msgid "_0dB" msgstr "" #: jack_mixer/channel.py:1263 msgid "New Input Channel" msgstr "" #: jack_mixer/channel.py:1296 msgid "_Color" msgstr "" #: jack_mixer/channel.py:1305 msgid "Input Channels" msgstr "" #: jack_mixer/channel.py:1307 msgid "_Display solo buttons" msgstr "" #: jack_mixer/channel.py:1329 msgid "New Output Channel" msgstr "" #: jack_mixer/channel.py:1397 msgid "Mute output channel send" msgstr "" #: jack_mixer/channel.py:1403 msgid "Solo output send" msgstr "" #: jack_mixer/channel.py:1407 msgid "P" msgstr "" #: jack_mixer/channel.py:1409 msgid "Pre (on) / Post (off) fader send" msgstr "" #: jack_mixer/gui.py:47 msgid "Use system setting" msgstr "" #: jack_mixer/gui.py:70 msgid "Cannot load PyXDG. " msgstr "" #: jack_mixer/gui.py:71 msgid "Your preferences will not be preserved across jack_mixer invocations." msgstr "" #: jack_mixer/gui.py:173 #, python-format msgid "Ignoring default_meter_scale setting, because '%s' scale is not known." msgstr "" #: jack_mixer/gui.py:184 #, python-format msgid "Ignoring default_slider_scale setting, because '%s' scale is not known." msgstr "" #: jack_mixer/preferences.py:30 msgid "Preferences" msgstr "" #: jack_mixer/preferences.py:53 msgid "" "Set the path where mixer project files are saved and loaded from by default" msgstr "" #: jack_mixer/preferences.py:59 jack_mixer/preferences.py:71 msgid "Default Project Path" msgstr "" #: jack_mixer/preferences.py:76 msgid "Set the interface language and localisation" msgstr "" #: jack_mixer/preferences.py:80 msgid "Language:" msgstr "" #: jack_mixer/preferences.py:83 msgid "Confirm quit" msgstr "" #: jack_mixer/preferences.py:85 msgid "Always ask for confirmation before quitting the application" msgstr "" #: jack_mixer/preferences.py:91 msgid "Use custom widgets" msgstr "" #: jack_mixer/preferences.py:93 msgid "Use widgets with custom design for the channel sliders" msgstr "" #: jack_mixer/preferences.py:99 msgid "Draw the volume meters with the selected solid color" msgstr "" #: jack_mixer/preferences.py:100 msgid "Use custom vumeter color" msgstr "" #: jack_mixer/preferences.py:113 msgid "Custom color:" msgstr "" #: jack_mixer/preferences.py:121 msgid "Reset the peak meters after the specified time" msgstr "" #: jack_mixer/preferences.py:122 msgid "Auto reset peak meter" msgstr "" #: jack_mixer/preferences.py:139 msgid "Time (s):" msgstr "" #: jack_mixer/preferences.py:149 msgid "" "Update the volume level meters with the specified interval in milliseconds" msgstr "" #: jack_mixer/preferences.py:152 msgid "Meter Refresh Period (ms):" msgstr "" #: jack_mixer/preferences.py:158 msgid "Interface" msgstr "" #: jack_mixer/preferences.py:164 msgid "Set the scale for all volume meters" msgstr "" #: jack_mixer/preferences.py:165 msgid "Meter scale:" msgstr "" #: jack_mixer/preferences.py:172 msgid "Set the scale for all volume sliders" msgstr "" #: jack_mixer/preferences.py:173 msgid "Slider scale:" msgstr "" #: jack_mixer/preferences.py:180 msgid "Scales" msgstr "" #: jack_mixer/preferences.py:187 msgid "" "Set how channel volume and balance are controlled via MIDI:\n" "\n" "- Jump To Value: channel volume or balance is set immediately to received " "controller value\n" "- Pick Up: control changes are ignored until a controller value near the " "current value is received\n" msgstr "" #: jack_mixer/preferences.py:191 msgid "Control Behavior:" msgstr "" #: jack_mixer/preferences.py:198 msgid "MIDI" msgstr "" #: jack_mixer/preferences.py:309 msgid "You need to restart the application for this setting to take effect." msgstr "" #: jack_mixer/scale.py:88 msgid "" "IEC 60268-18 Peak programme level meters - Digital audio peak level meter" msgstr "" #: jack_mixer/scale.py:115 msgid "" "IEC 60268-18 Peak programme level meters - Digital audio peak level meter, " "fewer marks" msgstr "" #: jack_mixer/scale.py:135 msgid "Linear scale with range from -70 to 0 dBFS" msgstr "" #: jack_mixer/scale.py:156 msgid "Linear scale with range from -30 to +30 dBFS" msgstr "" #: jack_mixer/scale.py:167 msgid "K20 scale" msgstr "" #: jack_mixer/scale.py:207 msgid "K14 scale" msgstr "" #: jack_mixer/serialization_xml.py:58 #, python-brace-format msgid "Document type '{type}' not supported." msgstr "" #: jack_mixer/slider.py:260 msgid "Center" msgstr "" #: jack_mixer/slider.py:263 #, python-brace-format msgid "Left: {left} / Right: {right}" msgstr "" #: /usr/lib/python3.9/argparse.py:296 msgid "usage: " msgstr "" #: /usr/lib/python3.9/argparse.py:856 msgid ".__call__() not defined" msgstr "" #: /usr/lib/python3.9/argparse.py:1199 #, python-format msgid "unknown parser %(parser_name)r (choices: %(choices)s)" msgstr "" #: /usr/lib/python3.9/argparse.py:1259 #, python-format msgid "argument \"-\" with mode %r" msgstr "" #: /usr/lib/python3.9/argparse.py:1268 #, python-format msgid "can't open '%(filename)s': %(error)s" msgstr "" #: /usr/lib/python3.9/argparse.py:1477 #, python-format msgid "cannot merge actions - two groups are named %r" msgstr "" #: /usr/lib/python3.9/argparse.py:1515 msgid "'required' is an invalid argument for positionals" msgstr "" #: /usr/lib/python3.9/argparse.py:1537 #, python-format msgid "" "invalid option string %(option)r: must start with a character " "%(prefix_chars)r" msgstr "" #: /usr/lib/python3.9/argparse.py:1555 #, python-format msgid "dest= is required for options like %r" msgstr "" #: /usr/lib/python3.9/argparse.py:1572 #, python-format msgid "invalid conflict_resolution value: %r" msgstr "" #: /usr/lib/python3.9/argparse.py:1590 #, python-format msgid "conflicting option string: %s" msgid_plural "conflicting option strings: %s" msgstr[0] "" msgstr[1] "" #: /usr/lib/python3.9/argparse.py:1656 msgid "mutually exclusive arguments must be optional" msgstr "" #: /usr/lib/python3.9/argparse.py:1723 msgid "positional arguments" msgstr "" #: /usr/lib/python3.9/argparse.py:1724 msgid "optional arguments" msgstr "" #: /usr/lib/python3.9/argparse.py:1739 msgid "show this help message and exit" msgstr "" #: /usr/lib/python3.9/argparse.py:1770 msgid "cannot have multiple subparser arguments" msgstr "" #: /usr/lib/python3.9/argparse.py:1822 /usr/lib/python3.9/argparse.py:2333 #, python-format msgid "unrecognized arguments: %s" msgstr "" #: /usr/lib/python3.9/argparse.py:1923 #, python-format msgid "not allowed with argument %s" msgstr "" #: /usr/lib/python3.9/argparse.py:1969 /usr/lib/python3.9/argparse.py:1983 #, python-format msgid "ignored explicit argument %r" msgstr "" #: /usr/lib/python3.9/argparse.py:2090 #, python-format msgid "the following arguments are required: %s" msgstr "" #: /usr/lib/python3.9/argparse.py:2105 #, python-format msgid "one of the arguments %s is required" msgstr "" #: /usr/lib/python3.9/argparse.py:2148 msgid "expected one argument" msgstr "" #: /usr/lib/python3.9/argparse.py:2149 msgid "expected at most one argument" msgstr "" #: /usr/lib/python3.9/argparse.py:2150 msgid "expected at least one argument" msgstr "" #: /usr/lib/python3.9/argparse.py:2154 #, python-format msgid "expected %s argument" msgid_plural "expected %s arguments" msgstr[0] "" msgstr[1] "" #: /usr/lib/python3.9/argparse.py:2212 #, python-format msgid "ambiguous option: %(option)s could match %(matches)s" msgstr "" #: /usr/lib/python3.9/argparse.py:2276 #, python-format msgid "unexpected option string: %s" msgstr "" #: /usr/lib/python3.9/argparse.py:2473 #, python-format msgid "%r is not callable" msgstr "" #: /usr/lib/python3.9/argparse.py:2490 #, python-format msgid "invalid %(type)s value: %(value)r" msgstr "" #: /usr/lib/python3.9/argparse.py:2501 #, python-format msgid "invalid choice: %(value)r (choose from %(choices)s)" msgstr "" #: /usr/lib/python3.9/argparse.py:2577 #, python-format msgid "%(prog)s: error: %(message)s\n" msgstr "" #: src/jack_mix_box.c:50 msgid "" "Usage: jack_mix_box [-n ] [-p] [-s] [-v ] MIDI_CC...\n" "\n" "-h|--help print this help message\n" "-n|--name set JACK client name\n" "-p|--pickup enable MIDI pickup mode (default: jump-to-value)\n" "-s|--stereo make all input channels stereo with left+right input\n" "-v|--volume initial volume gain in dBFS (default 0.0, i.e. unity gain)\n" "\n" "Each positional argument is interpreted as a MIDI Control Change number and\n" "adds a mixer channel with one (mono) or left+right (stereo) inputs, whose\n" "volume can be controlled via the given MIDI Control Change.\n" "\n" "Send SIGUSR1 to the process to have the current volumes reported per input\n" "channel.\n" "\n" msgstr "" #: src/jack_mix_box.c:134 #, c-format msgid "Unknown argument, aborting.\n" msgstr "" #: src/jack_mix_box.c:140 msgid "You must specify at least one input channel.\n" msgstr "" #: src/jack_mix_box.c:176 #, c-format msgid "Failed to add channel %d, aborting.\n" msgstr "" #. JACK_MIXER_NO_ERROR #: src/jack_mixer.c:222 msgid "No error.\n" msgstr "" #. JACK_MIXER_ERROR_JACK_CLIENT_CREATE #: src/jack_mixer.c:224 msgid "" "Could not create JACK client.\n" "Please make sure JACK daemon is running.\n" msgstr "" #. JACK_MIXER_ERROR_JACK_MIDI_IN_CREATE #: src/jack_mixer.c:226 msgid "Could not create JACK MIDI in port.\n" msgstr "" #. JACK_MIXER_ERROR_JACK_MIDI_OUT_CREATE #: src/jack_mixer.c:228 msgid "Could not create JACK MIDI out port.\n" msgstr "" #. JACK_MIXER_ERROR_JACK_SET_PROCESS_CALLBACK #: src/jack_mixer.c:230 msgid "Could not set JACK process callback.\n" msgstr "" #. JACK_MIXER_ERROR_JACK_SET_BUFFER_SIZE_CALLBACK #: src/jack_mixer.c:232 msgid "Could not set JACK buffer size callback.\n" msgstr "" #. JACK_MIXER_ERROR_JACK_ACTIVATE #: src/jack_mixer.c:234 msgid "Could not activate JACK client.\n" msgstr "" #. JACK_MIXER_ERROR_CHANNEL_MALLOC #: src/jack_mixer.c:236 msgid "Could not allocate memory for channel.\n" msgstr "" #. JACK_MIXER_ERROR_CHANNEL_NAME_MALLOC #: src/jack_mixer.c:238 msgid "Could not allocate memory for channel name.\n" msgstr "" #. JACK_MIXER_ERROR_PORT_REGISTER #: src/jack_mixer.c:240 msgid "Could not register JACK port for channel.\n" msgstr "" #. JACK_MIXER_ERROR_PORT_REGISTER_LEFT #: src/jack_mixer.c:242 msgid "Could not register JACK port for left channel.\n" msgstr "" #. JACK_MIXER_ERROR_PORT_REGISTER_RIGHT #: src/jack_mixer.c:244 msgid "Could not register JACK port for right channel.\n" msgstr "" #. JACK_MIXER_ERROR_JACK_RENAME_PORT #: src/jack_mixer.c:246 msgid "Could not rename JACK port for channel.\n" msgstr "" #. JACK_MIXER_ERROR_JACK_RENAME_PORT_LEFT #: src/jack_mixer.c:248 msgid "Could not rename JACK port for left channel.\n" msgstr "" #. JACK_MIXER_ERROR_JACK_RENAME_PORT_LEFT #: src/jack_mixer.c:250 msgid "Could not rename JACK port for right channel.\n" msgstr "" #. JACK_MIXER_ERROR_PORT_NAME_MALLOC #: src/jack_mixer.c:252 msgid "Could not allocate memory for port name.\n" msgstr "" #. JACK_MIXER_ERROR_INVALID_CC #: src/jack_mixer.c:254 msgid "Control Change number out of range.\n" msgstr "" #. JACK_MIXER_ERROR_NO_FREE_CC #: src/jack_mixer.c:256 msgid "No free Control Change number.\n" msgstr "" #: src/jack_mixer.c:811 #, c-format msgid "%s: volume is %f dbFS for mixer channel: %s\n" msgstr "" jack_mixer-release-17/data/locale/meson.build000066400000000000000000000011131413224161500213420ustar00rootroot00000000000000if get_option('gui').enabled() languages = ['de', 'es', 'fr'] msgfmt = find_program('msgfmt') foreach lang : languages target = meson.project_name() + '-' + lang po_file = target + '.po' mo_file = meson.project_name() + '.mo' msg_dir = join_paths(get_option('localedir'), lang, 'LC_MESSAGES') mo = configure_file( input: po_file, output: target + '.mo', command: [msgfmt, '-o', '@OUTPUT@', '@INPUT@'], ) install_data(mo, rename: mo_file, install_dir: msg_dir) endforeach endif jack_mixer-release-17/data/meson.build000066400000000000000000000012501413224161500201050ustar00rootroot00000000000000# Menu .desktop file desktop_file = 'jack_mixer.desktop' install_data( desktop_file, install_dir: desktopdir ) # RaySession template raysession_template = 'client_templates.xml' install_data( raysession_template, install_dir: raysessiondir) # Application icons sizes = [ '16x16', '22x22', '24x24', '32x32', '48x48', ] foreach size : sizes install_data( join_paths('art', size, 'jack_mixer.png'), install_dir: join_paths(icondir, size, 'apps') ) endforeach install_data( join_paths('art', 'scalable', 'jack_mixer.svg'), install_dir: join_paths(icondir, 'scalable', 'apps') ) # Translations subdir('locale') jack_mixer-release-17/docs/000077500000000000000000000000001413224161500157645ustar00rootroot00000000000000jack_mixer-release-17/docs/CONTRIBUTING.md000066400000000000000000000145121413224161500202200ustar00rootroot00000000000000# Contributing Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. ## Types of Contributions You can contribute in many ways: ### Report Bugs Report bugs at . If you are reporting a bug, please include: - Your operating system name and version. - The **jack_mixer** version (see the "About" dialog). - The Python version (`python --version`). - A description of what went wrong ("X doesn't work" is *not enough!*). - Detailed steps to reproduce the bug. - Any details about your local setup that might be helpful in troubleshooting. ### Fix Bugs or Implement Features and Enhancements Look through the GitHub issues for for anything tagged with "bug", "feature request" or "enhancement" and if you think you can help out, leave a comment on the issue saying what you intend to work on and in which timeframe. ### Write Documentation **jack_mixer** could definitely use more documentation, whether it is more detailed man pages or a real user guide, better docstrings or comments in the code, or even third-party tutorials, video or quick tips on social media. If you want to help out with the documentation, please get in touch with the **jack_mixer** maintainers via the Github issue tracker, email or on IRC (see [README] for contact information) ### Add or Update Translations We would like to translate the user interface of **jack_mixer** in as many languages as possible and keep existing translations as up-to-date and error-free as possible. See the [Translations](#translations) section below. ### Submit Feedback The best way to send feedback is to also file an issue at . If you are proposing a feature: - Give a short and poignant title to your feature request issue. - Explain in detail how it would work. - Keep the scope as narrow as possible, to make it easier to implement. - Remember that this is a volunteer-driven project, and that contributions are welcome :) ## Development ### Development Environment Setup and Workflow Ready to work on **jack_mixer**? First you should set up your local development environment: 1. Fork the [jack-mixer/jack_mixer] repo on GitHub. 2. Clone your fork locally: $ git clone git@github.com:your_name_here/jack_mixer.git 3. Install all build and run-time requirements listed in the [INSTALL] file and make sure you can configure and build the application with `meson` as described in the same document. 4. Install `flake8`, `isort` and `black` either via your distribution's package management or with `pip install`. 5. Create a branch for local development of your new feature or bugfix: $ git checkout -b bugfix/what-does-this-fix or: $ git checkout -b feature/what-does-this-do Now you're ready to make your changes, make sure you follow the [Coding Guidelines](#coding-guidelines) outlined below. 6. Commit your changes and push your branch to GitHub: $ git add . $ git commit -m "Detailed description of your changes." $ git push -u origin name-of-your-bugfix-or-feature 7. Submit a pull request through the GitHub website. Create a new branch for every new PR, starting from the `main` branch. ### Coding Guidelines #### Python Code - Check all Python code with `flake8` and fix all warnings and errors or explicitly silence them (and be ready to explain why you did this when your changes are reviewed). - Format all Python code with `isort` and `black`. - Line-endings should should be Unix style (`\n`), not Windows style (`\r\n`). - Cython files should follow the same formatting rules as Python source code, where possible. - The Python code must work for all supported Python 3 versions (see `pyproject.toml`). #### C Code - Opening brackets go on their own line: ``` if (condition) { stuff; } ``` - There should be a space between keywords and parenthesis for: `if`, `else`, `while`, `switch`, `catch`, `function`. - Function calls have no space before the parentheses. - No spaces are left inside the parentheses. - A space after each comma, but without space before. - All binary operators must have one space before and one after. - There should be no empty comments. These conventions may change in the future and we may introduce auto-formatting of C code with `clang-format` at some point. ## Translations ### Adding a New Translation 1. Copy `data/local/jack_mixer.pot` to `data/locale/jack_mixer-.po`, where `` is the [two- or three-letter code] for the language of the new translation. 2. Edit `data/local/meson.build` and add a string element with the language code for the new translation to the `languages` [array](https://mesonbuild.com/Syntax.html#arrays) (keep it sorted alphabetically). 3. Edit `jack-mixer-.po` and translate all messages (you only need to translate the messages for `argparse`, which are used in the command line help text). 4. Run `./tools/compile-messages.py` to compile all `*.po` files to `*.mo` files. 5. Build the application with `meson` and then run it from the root of the source directory using the `./tools/jack_mixer.sh` script and check your translations. Also use the `-h` command line option to check the translation of the usage help message. 6. Add a `Comment` tag in the new language to the `data/jack_mixer.desktop` file. 7. Commit the `jack-mixer-.po` file and your changes to the `.desktop` and `data/locale/meson.build` files to a new branch and make a Pull Request. ### Updating a Translation When the timestamp listed in `data/local/jack_mixer.pot` is newer than a translation `.po` file, it may contain updated or new messages, which need to be translated. Run the `./tools/merge-messages.py` script to update all `*.po` files and then edit the `.po` for the language translation you want to update. Use `git diff jack-mixer-.po` to check for new or updated messages. When you have made your edits, check the new translations as described above and make a new Pull Request (only include the `.po` files, which you edited). [jack-mixer/jack_mixer]: https://github.com/jack-mixer/jack_mixer [INSTALL]: ../INSTALL.md [README]: ../README.md [two- or three-letter code]: https://www.gnu.org/software/gettext/manual/html_node/Language-Codes.html#Language-Codesjack_mixer-release-17/docs/jack_mix_box.1.rst.in000066400000000000000000000067711413224161500217320ustar00rootroot00000000000000============== jack_mix_box ============== --------------------------------------------------- A minimal JACK audio mixer with MIDI volume control --------------------------------------------------- :Author: Nedko Arnaudov (original author), Frédéric Peters (current maintainer). :Date: 2021-04-14 :Copyright: GNU General Public License Version 2 :Version: @VERSION@ :Manual section: 1 :Manual group: audio SYNOPSIS ======== jack_mix_box [-n ] [-p] [-s] [-v ] MIDI_CC... DESCRIPTION =========== This manual page documents the **jack_mix_box** command. **jack_mix_box** is a minimal, GUI-less version of **jack_mixer**. It creates a JACK client with stereo output ports and a fixed number of mono or stereo input ports. The number of input ports and the MIDI Control Change message controlling the volume level of each is set via the command line. The signal of each input channel is mixed into the stereo outputs. OPTIONS ======= Positional arguments: MIDI_CC... add input channel controlled by MIDI_CC Optional arguments: -h, --help print this help message -n, --name set JACK client name -p, --pickup enable MIDI pickup mode (default: jump-to-value) -s, --stereo make all input channels stereo with left+right input -v, --volume initial volume gain in dBFS (default 0.0, i.e. unity gain) USAGE ===== Each positional argument is interpreted as a MIDI Control Change number and adds a mixer channel with one (mono) or left+right (stereo) inputs, whose volume can be controlled via the given MIDI Control Change. Use ``jack_connect`` or any other JACK conmection manager to connect other JACK clients or system ports to the input and output port. Send a ``SIGUSR1`` signal to the ``jack_mix_box`` process to have the current volumes per input channel reported to the standard output. JACK AUDIO AND MIDI PORTS ------------------------- * For each input channel, **jack_mix_box** will create one (if it is a mono channel) or two (stereo) JACK audio input ports. The input port(s) will be named "Channel ", where "" is the channel number, and have an " L" resp. " R" suffix, it the channel is stereo. * It will create two JACK audio output ports named "MAIN L" and "MAIN R". * It will also create one JACK MIDI input ("midi in") and one MIDI output ("midi out") port. ENVIRONMENT =========== ``LANGUAGE``, ``LC_ALL``, ``LC_MESSAGES``, and ``LANG`` The first of these environment variables set to a non-empty value is used to determine the language(s) for loading gettext translation files. The value should be a colon separated list of language codes. ``LC_NUMERIC`` The language set via this environment determines the floating point number format used to parse volume levels given on the command line and to format them in the output. If ``LC_NUMERIC`` is not set, its value is inherited from one of the language environment variables described above. ``LOCALEDIR`` With this environment variable, the base directory for the gettext translation files can be changed from the default set at compile time, which is usually ``/share/locale``. SIGNALS ======= ``SIGUSR1`` Send ``SIGUSR1`` to the **jack_mix_box** process to have the current volumes reported per input channel. SEE ALSO ======== * Project homepage (https://rdio.space/jackmixer/) * Source code repository on GitHub (https://github.com/jack-mixer/jack_mixer) * JACK Audio Connection Kit (https://jackaudio.org/) jack_mixer-release-17/docs/jack_mixer.1.rst.in000066400000000000000000000245021413224161500214010ustar00rootroot00000000000000============= jack_mixer ============= ---------------------------------------------- A graphical multi-channel audio mixer for JACK ---------------------------------------------- :Author: Nedko Arnaudov (original author), Frédéric Peters (current maintainer). :Date: 2021-04-14 :Copyright: GNU General Public License Version 2 :Version: @VERSION@ :Manual section: 1 :Manual group: audio SYNOPSIS ======== jack_mixer.py [-h] [-c FILE] [-d] [NAME] DESCRIPTION =========== This manual page documents the **jack_mixer** command. **jack_mixer** is a multi-channel audio mixer application for the JACK Audio Connection Kit with a graphical user interface and has a look & handling similar to hardware mixing desks. OPTIONS ======= Positional arguments: NAME set JACK client name Optional arguments: -h, --help show this help message and exit -c FILE, --config FILE load mixer project configuration from FILE -d, --debug enable debug logging messages GUI USAGE ========= After starting **jack_mixer**, you need to create at least one input and one output channel via the respective entries in the "Mixer" menu (see section **MAIN MENU**). Then connect your audio sources to the **jack_mixer's** JACK audio input ports (see section **JACK AUDIO AND MIDI PORTS**) using ``jack_connect`` or GUI tools like the QJackCtl's connections window, Catia, Carla, etc. Connect the monitor channel output port(s) or the output ports of the output channels to the input ports of your audio interface or any other JACK audio input ports. Then adjust input and output channel volume levels and balance as required using the fader controls in each channel strip (see section **MOUSE BINDINGS** for ways to to control the faders and routing). JACK AUDIO AND MIDI PORTS ------------------------- * For each input channel, **jack_mixer** will create one (if it is a mono channel) or two (stereo) JACK audio input ports. The input port(s) will be named like the input channel strip, if it is a mono channel, or with an " L" resp. " R" suffix, it it is a stereo channel. * Optionally, for each input channel, it creates a direct, post-fader audio output port (mono) or pair of output ports (stereo). These are named the same as their input port(s) with " Out" resp. " Out L" / "Out R" appended. * For each ouput channel it will create one (mono) or two (stereo) audio output ports. * Additionally it will create one pair of audio monitor ports named "Monitor L" and "Monitor R". * If **jack_mixer** was compiled with JACK MIDI support, it will also create one MIDI input ("midi in") and one MIDI output ("midi out") port. MAIN MENU --------- Main Menu: Use the **Mixer** menu to: * Add an input or output channel. * Add an output channel. * Open a file with a preset mixer layout. * Save the current mixer layout to an XML file. * Exit **jack_mixer**. Use the **Edit** menu to: * Change the properties of an input or output channel. * Remove an input or output channel. * Toggle all channel strips between wide and narrow view. * Clear the current mixer layout, i.e. remove all input and output channels. * Open the setting preferences dialog. Use the **Help** menu to show **jack_mixer's** about dialog. MOUSE BINDINGS -------------- Main window: * Left-click and drag the vertical divider line horizontally to set the size distribution of the left and right panels for the input and output channel strips. If a panel contains more channel strips than can be fit into its horizontal size, horizontal scrollbars will be available at the bottom of the panel. Channel strip header: * Double click the channel strip header to open the channel properties dialog. * Ctrl+left-click the channel strip header to toggle the channel strip between wide and narrow view. * Left-click and drag the channel strip header horizontally to re-order channel strips. This doesn't effect the order of the JACK output ports and input channels are always arranged on the left side of the main **jack_mixer** window and output chanels on the right side. Control groups (input channels): Each input channel header has a control group for each output channel, which has the same background color and label as the corresponding output channel header. Each output channel control group shows a pre/post-fader ("P") and a mute ("M") button and, optionally, a solo ("S") button. * Left-click the pre/post-fader ("P") button to toggle the signal sent to the corresponding output channel between the post-fader (off) and pre-fader (on) signal of the input channel. * Left-click the mono ("M") button to mute the signal from the input channel going to the corresponding output channel. * Left-click the solo ("S") button to solo the input channel for the corresponding output channel. i.e. only the signal from this input channel (plus any other soloed channels) is going to this output channel. Volume read-out and peak-indicator: * Left-click the volume level read-out to enter a value manually and press Enter to set it. * Left/right-click the peak volume level read-out to reset the over-zero indication. * Middle-click the peak volume level read-out to set the volume level to peak at 0 dB at the current signal input level. Volume meter: * Left-click the pre-fader ("PRE") button below the volume meter to switch the signal, which is metered, from post-fader (off) to pre-fader (on). *(not yet implemented)* Volume slider: * Left-click and drag vertically anywhere in the slider area to set the channel output volume level. * Scroll the mouse wheel up or down over the slider area to increase or decrease the volume level. * Right-click anywhere in the slider area to move the volume level slowly towards the click position. * Double click to set the volume level to -inf. * Ctrl+left-click to set the volume level to 0 dBFS. Balance slider: * Left-click and drag horizontally anywhere in the slider area to set the balance between the left and right channel of the output signal. * Scroll the mouse wheel up or down over the slider area to move the balance to right or left. * Right-click anywhere in the slider area to move the balance slowly towards the click position. * Double click to set the left/right balance to center. Channel buttons: * Left-click the mono ("M") button to mute signal from channel going to all output channels (including direct channel outs). * Ctrl+left-click the mono ("M") button for "exclusive" mute, i.e. the mute function will be activated on this input or output channel only and deactivated on all other input resp. output channels. * Left-click the solo ("S") button to solo an input channel, i.e. only the signal from this channel is going to all output channels (including the direct outputs of the channel). The solo function is cumulative, i.e. you can activate solo on more than one input channel and the signals from all soloed channels will be going to each output channel. Output channels have no solo button. * Ctrl+left-click the solo ("SM") button for "exclusive" solo, i.e. the solo function will be activated on this channel only and deactivated on all other input channels. * Right-clicking the mute ("M") button acts like left-clicking but also synchronizes the muted state on all output channel control groups of the channel with the main channel mute button. Output channel control groups, where the mute function was already active before righ-clicking the mute button are unaffected, i.e. it will stay activated. * Right-clicking the solo ("S") button acts like left-clicking but also synchronizes the soloed state on all output channel control groups of the channel with the main channel solo button. Output channel control groups, where the solo function was already active before righ-clicking the solo button are unaffected, i.e. it will stay activated. * Left-click the monitor ("MON") button, to toggle monitoring. If monitoring is on, the (post-fader) signal from the channel is sent to the "Monitor L/R" outputs. The monitor function is exclusive, i.e. activating monitoring on a channel, will turn off monitoring on any other channel. COMPATIBILITY ============= **jack_mixer** is fully compatible with the New Session Manager (NSM) protocol and provides Level 1 (L1) support for the LADISH protocol. When running as an NSM client, the mixer layout and current state will be saved in the NSM session. When started as an L1 client by LADISH, the mixer layout and current state will be saved in the LADISH studio session or project (the first time LADISH requests **jack_mixer** to save a project, it will open a "Save as" dialog). FILES ===== ``/jack_mixer/preferences.ini`` This file stores global settings for **jack_mixer**. ENVIRONMENT =========== ``LANGUAGE``, ``LC_ALL``, ``LC_MESSAGES``, and ``LANG`` The first of these environment variables set to a non-empty value is used to determine the language(s) for loading gettext translation files if the language is not set in the global settings. The value should be a colon separated list of language codes. ``LOCALEDIR`` With this environment variable, the base directory for the gettext translation files can be changed from the default set at compile time, which is usually ``/share/locale``. ``JACK_MIXER_DEBUG`` When this environment variable is set, the logging level in the Python layer is set to ``DEBUG`` unless it is overwritten by the ``-d|--debug`` command line switch. ``NSM_URL`` When this environment variable is set, **jack_mixer** will act as a New Session Manager (NSM) session client. SIGNALS ======= ``SIGUSR1`` In accordance with Level 1 support of LADISH, **jack_mixer** saves the current mixer layout and state on receiving a ``USR1`` signal. ``SIGINT | SIGTERM`` When receiving an ``INT`` or ``TERM`` signal, **jack_mixer** will either exit the application immediately or, if enabled in the preferences, will show a confirmation dialog, allowing the user to either quit or cancel the action. SEE ALSO ======== * Project homepage (https://rdio.space/jackmixer/) * Source code repository on GitHub (https://github.com/jack-mixer/jack_mixer) * JACK Audio Connection Kit (https://jackaudio.org/) * New Session Manager (https://new-session-manager.jackaudio.org/) jack_mixer-release-17/docs/meson.build000066400000000000000000000035631413224161500201350ustar00rootroot00000000000000if get_option('gui').enabled() fs = import('fs') jack_mixer_man = 'jack_mixer.1' jack_mix_box_man = 'jack_mix_box.1' rst2man = find_program('rst2man', 'rst2man.py', required: false) if fs.exists(jack_mixer_man) install_man(jack_mixer_man) elif rst2man.found() jack_mixer_man_rst_in = 'jack_mixer.1.rst.in' jack_mixer_man_rst = configure_file( input: jack_mixer_man_rst_in, output: 'jack_mixer.1.rst', configuration: { 'VERSION': meson.project_version() } ) jack_mixer_troff = custom_target( 'jack_mixer_rst2man', output: jack_mixer_man, input: jack_mixer_man_rst, command: [rst2man, '@INPUT@', '@OUTPUT@'], install: true, install_dir: join_paths(get_option('mandir'), 'man1') ) else error('Pre-generated file \'@0@\' and \'rst2man\' not found.\n'.format(jack_mixer_man) + 'Please install \'docutils\' from https://pypi.org/project/docutils.') endif if fs.exists(jack_mix_box_man) install_man(jack_mix_box_man) elif rst2man.found() jack_mix_box_man_rst_in = 'jack_mix_box.1.rst.in' jack_mix_box_man_rst = configure_file( input: jack_mix_box_man_rst_in, output: 'jack_mix_box.1.rst', configuration: { 'VERSION': meson.project_version() } ) jack_mix_box_troff = custom_target( 'jack_mix_box_rst2man', output: jack_mix_box_man, input: jack_mix_box_man_rst, command: [rst2man, '@INPUT@', '@OUTPUT@'], install: true, install_dir: join_paths(get_option('mandir'), 'man1') ) endif meson.add_dist_script('meson_dist_rst2man.py', jack_mixer_man, jack_mix_box_man) endif jack_mixer-release-17/docs/meson_dist_rst2man.py000077500000000000000000000023041413224161500221520ustar00rootroot00000000000000#!/usr/bin/env python """Create/Update man page(s) from ReST source file(s) to be included in source distribution.""" import argparse import shutil import sys from os import chdir, environ, getcwd from os.path import join from subprocess import run build_root = environ.get("MESON_BUILD_ROOT") dist_root = environ.get("MESON_DIST_ROOT") source_root = environ.get("MESON_SOURCE_ROOT") ap = argparse.ArgumentParser() ap.add_argument("-v", "--verbose", action="store_true", help="Be more verbose.") ap.add_argument("man_page", nargs="*", help="Man page(s) to create.") args = ap.parse_args() if args.verbose: print("cwd:", getcwd()) print("build root:", build_root) print("dist root:", dist_root) print("source root:", source_root) print("sys.argv:", sys.argv) for man in args.man_page: target = join("docs", man) dst = join(dist_root, "docs", man) print("Creating man page '{}'".format(target)) cmd = ["ninja"] if args.verbose: cmd += ["-v"] cmd += [target] chdir(build_root) proc = run(cmd) if proc.returncode != 0: sys.exit("'ninja' returned non-zero ({}) for target '{}'.".format(proc.returncode, target)) shutil.copy(target, dst) jack_mixer-release-17/jack_mixer/000077500000000000000000000000001413224161500171505ustar00rootroot00000000000000jack_mixer-release-17/jack_mixer/__main__.py000066400000000000000000000001721413224161500212420ustar00rootroot00000000000000#!/usr/bin/env python3 import sys from jack_mixer.app import main if __name__ == "__main__": sys.exit(main() or 0) jack_mixer-release-17/jack_mixer/abspeak.py000066400000000000000000000074561413224161500211440ustar00rootroot00000000000000# This file is part of jack_mixer # # Copyright (C) 2006 Nedko Arnaudov # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. import math from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GObject from gi.repository import GLib class AbspeakWidget(Gtk.EventBox): def __init__(self, app): super().__init__() self.label = Gtk.Label() self.add(self.label) self.connect("button-press-event", self.on_mouse) self.peak = -math.inf self.gui_factory = app.gui_factory self.gui_factory.connect( "auto-reset-peak-meters-changed", self.on_auto_reset_peak_meters_changed ) self.gui_factory.connect( "auto-reset-peak-meters-time-seconds-changed", self.on_auto_reset_peak_meters_time_seconds_changed ) self.reset_timer_id = None def emit_reset(self): self.emit("reset") context = self.get_style_context() context.remove_class("over_zero") context.remove_class("is_nan") self.reset_timer_id = None return False def reset_timer(self): if self.reset_timer_id is not None: GLib.source_remove(self.reset_timer_id) self.reset_timer_id = GLib.timeout_add( self.gui_factory.auto_reset_peak_meters_time_seconds * 1000, self.emit_reset ) def on_auto_reset_peak_meters_changed(self, widget, event): if event is True: self.reset_timer() def on_auto_reset_peak_meters_time_seconds_changed(self, widget, event): self.reset_timer() def get_style_context(self): return self.label.get_style_context() def on_mouse(self, widget, event): if event.type == Gdk.EventType.BUTTON_PRESS: if event.button == 1 or event.button == 2 or event.button == 3: context = self.get_style_context() context.remove_class("over_zero") context.remove_class("is_nan") if event.button == 1 or event.button == 3: self.emit("reset") elif event.button == 2: adjust = -self.peak if abs(adjust) < 30: # we better don't adjust more than +- 30 dB self.emit("volume-adjust", adjust) def set_peak(self, peak): if self.gui_factory.auto_reset_peak_meters: if peak > self.peak: self.reset_timer() self.peak = peak context = self.get_style_context() if math.isnan(peak): context.remove_class("over_zero") context.add_class("is_nan") self.label.set_text("NaN") else: # TODO: l10n text = "%+.1f" % peak context.remove_class("is_nan") if peak > 0: context.add_class("over_zero") else: context.remove_class("over_zero") self.label.set_text(text) GObject.signal_new( "reset", AbspeakWidget, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [] ) GObject.signal_new( "volume-adjust", AbspeakWidget, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [GObject.TYPE_FLOAT], ) jack_mixer-release-17/jack_mixer/app.py000066400000000000000000001315461413224161500203140ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # This file is part of jack_mixer # # Copyright (C) 2006-2009 Nedko Arnaudov # Copyright (C) 2009-2021 Frederic Peters et al. # import getpass import gettext import logging import datetime import os import re import signal import sys from os.path import abspath, dirname, isdir, isfile, join from urllib.parse import urlparse import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk from gi.repository import GLib from . import gui from . import scale from ._jack_mixer import Mixer from .channel import InputChannel, NewInputChannelDialog, NewOutputChannelDialog, OutputChannel from .nsmclient import NSMClient from .preferences import PreferencesDialog from .serialization_xml import XmlSerialization from .serialization import SerializedObject, Serializator from .styling import load_css_styles from .version import __version__ __program__ = "jack_mixer" # A "locale" directory present within the package take precedence _pkglocdir = join(abspath(dirname(__file__)), "locale") # unless overwritten by the "LOCALEDIR environment variable. # Fall back to the system default locale directory. _localedir = os.environ.get("LOCALEDIR", _pkglocdir if isdir(_pkglocdir) else None) translation = gettext.translation(__program__, _localedir, fallback=True) translation.install() log = logging.getLogger(__program__) __doc__ = _("A multi-channel audio mixer application for the JACK Audio Connection Kit.") __license__ = _("""\ jack_mixer is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. jack_mixer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with jack_mixer; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-130159 USA """) # Hack argparse and delay its import to get it to use our translations gettext.gettext, gettext.ngettext = translation.gettext, translation.ngettext import argparse def add_number_suffix(s): def inc(match): return str(int(match.group(0)) + 1) new_s = re.sub(r"(\d+)\s*$", inc, s) if new_s == s: new_s = s + " 1" return new_s class JackMixer(SerializedObject): # scales suitable as meter scales meter_scales = [ scale.K20(), scale.K14(), scale.IEC268(), scale.Linear70dB(), scale.IEC268Minimalistic(), ] # scales suitable as volume slider scales slider_scales = [scale.Linear30dB(), scale.Linear70dB()] def __init__(self, client_name=__program__): self.visible = False self.nsm_client = None # name of project file that is currently open self.current_filename = None self.last_project_path = None self._monitored_channel = None self._init_solo_channels = None if os.environ.get("NSM_URL"): self.nsm_client = NSMClient( prettyName=__program__, saveCallback=self.nsm_save_cb, openOrNewCallback=self.nsm_open_cb, supportsSaveStatus=False, hideGUICallback=self.nsm_hide_cb, showGUICallback=self.nsm_show_cb, exitProgramCallback=self.nsm_exit_cb, loggingLevel="error", ) self.nsm_client.announceGuiVisibility(self.visible) else: self.visible = True self.create_mixer(client_name, with_nsm=False) def create_mixer(self, client_name, with_nsm=True): self.mixer = Mixer(client_name) self.create_ui(with_nsm) self.window.set_title(client_name) # Port names, which are not user-settable are not marked as translatable, # so they are the same regardless of the language setting of the environment # in which a project is loaded. self.monitor_channel = self.mixer.add_output_channel("Monitor", True, True) self.meter_refresh_period = \ self.gui_factory.get_meter_refresh_period_milliseconds() self.meter_refresh_timer_id = GLib.timeout_add(self.meter_refresh_period, self.read_meters) GLib.timeout_add(50, self.midi_events_check) if with_nsm: GLib.timeout_add(200, self.nsm_react) def cleanup(self): log.debug("Cleaning jack_mixer.") if not self.mixer: return for channel in self.channels: channel.unrealize() self.mixer.destroy() # --------------------------------------------------------------------------------------------- # UI creation and (de-)initialization def new_menu_item(self, title, callback=None, accel=None, enabled=True): menuitem = Gtk.MenuItem.new_with_mnemonic(title) menuitem.set_sensitive(enabled) if callback: menuitem.connect("activate", callback) if accel: self.menu_item_add_accelerator(menuitem, accel) return menuitem def menu_item_add_accelerator(self, menuitem, accel): key, mod = Gtk.accelerator_parse(accel) menuitem.add_accelerator( "activate", self.menu_accelgroup, key, mod, Gtk.AccelFlags.VISIBLE ) def create_recent_file_menu(self): def filter_func(item): return ( item.mime_type in ("text/xml", "application/xml") and __program__ in item.applications ) filter_flags = Gtk.RecentFilterFlags.MIME_TYPE | Gtk.RecentFilterFlags.APPLICATION recentfilter = Gtk.RecentFilter() recentfilter.set_name(_("jack_mixer XML files")) recentfilter.add_custom(filter_flags, filter_func) recentchooser = Gtk.RecentChooserMenu.new_for_manager(self.recentmanager) recentchooser.set_sort_type(Gtk.RecentSortType.MRU) recentchooser.set_local_only(True) recentchooser.set_limit(10) recentchooser.set_show_icons(True) recentchooser.set_show_numbers(True) recentchooser.set_show_tips(True) recentchooser.add_filter(recentfilter) recentchooser.connect("item-activated", self.on_recent_file_chosen) recentmenu = Gtk.MenuItem.new_with_mnemonic(_("_Recent Projects")) recentmenu.set_submenu(recentchooser) return recentmenu def create_ui(self, with_nsm): self.channels = [] self.output_channels = [] load_css_styles() # Main window self.width = 420 self.height = 420 self.paned_position = 210 self.window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL) self.window.set_icon_name(__program__) self.window.set_default_size(self.width, self.height) self.gui_factory = gui.Factory(self.window, self.meter_scales, self.slider_scales) self.gui_factory.connect("language-changed", self.on_language_changed) self.gui_factory.emit("language-changed", self.gui_factory.get_language()) self.gui_factory.connect("midi-behavior-mode-changed", self.on_midi_behavior_mode_changed) self.gui_factory.connect( "default-meter-scale-changed", self.on_default_meter_scale_changed ) self.gui_factory.connect( "meter-refresh-period-milliseconds-changed", self.on_meter_refresh_period_milliseconds_changed ) self.gui_factory.emit_midi_behavior_mode() # Recent files manager self.recentmanager = Gtk.RecentManager.get_default() self.vbox_top = Gtk.VBox() self.window.add(self.vbox_top) self.menu_accelgroup = Gtk.AccelGroup() self.window.add_accel_group(self.menu_accelgroup) # Main Menu self.menubar = Gtk.MenuBar() self.vbox_top.pack_start(self.menubar, False, True, 0) mixer_menu_item = Gtk.MenuItem.new_with_mnemonic(_("_Mixer")) self.menubar.append(mixer_menu_item) edit_menu_item = Gtk.MenuItem.new_with_mnemonic(_("_Edit")) self.menubar.append(edit_menu_item) help_menu_item = Gtk.MenuItem.new_with_mnemonic(_("_Help")) self.menubar.append(help_menu_item) # Mixer (and File) menu self.mixer_menu = Gtk.Menu() mixer_menu_item.set_submenu(self.mixer_menu) self.mixer_menu.append( self.new_menu_item(_("New _Input Channel"), self.on_add_input_channel, "N") ) self.mixer_menu.append( self.new_menu_item( _("New Output _Channel"), self.on_add_output_channel, "N" ) ) self.mixer_menu.append(Gtk.SeparatorMenuItem()) if not with_nsm: self.mixer_menu.append( self.new_menu_item(_("_Open..."), self.on_open_cb, "O") ) # Recent files sub-menu self.mixer_menu.append(self.create_recent_file_menu()) self.mixer_menu.append(self.new_menu_item(_("_Save"), self.on_save_cb, "S")) if not with_nsm: self.mixer_menu.append( self.new_menu_item(_("Save _As..."), self.on_save_as_cb, "S") ) self.mixer_menu.append(Gtk.SeparatorMenuItem()) if with_nsm: self.mixer_menu.append(self.new_menu_item(_("_Hide"), self.nsm_hide_cb, "W")) else: self.mixer_menu.append(self.new_menu_item(_("_Quit"), self.on_quit_cb, "Q")) # Edit menu edit_menu = Gtk.Menu() edit_menu_item.set_submenu(edit_menu) self.channel_edit_input_menu_item = self.new_menu_item( _("_Edit Input Channel"), enabled=False ) edit_menu.append(self.channel_edit_input_menu_item) self.channel_edit_input_menu = Gtk.Menu() self.channel_edit_input_menu_item.set_submenu(self.channel_edit_input_menu) self.channel_edit_output_menu_item = self.new_menu_item( _("E_dit Output Channel"), enabled=False ) edit_menu.append(self.channel_edit_output_menu_item) self.channel_edit_output_menu = Gtk.Menu() self.channel_edit_output_menu_item.set_submenu(self.channel_edit_output_menu) self.channel_remove_input_menu_item = self.new_menu_item( _("_Remove Input Channel"), enabled=False ) edit_menu.append(self.channel_remove_input_menu_item) self.channel_remove_input_menu = Gtk.Menu() self.channel_remove_input_menu_item.set_submenu(self.channel_remove_input_menu) self.channel_remove_output_menu_item = self.new_menu_item( _("Re_move Output Channel"), enabled=False ) edit_menu.append(self.channel_remove_output_menu_item) self.channel_remove_output_menu = Gtk.Menu() self.channel_remove_output_menu_item.set_submenu(self.channel_remove_output_menu) edit_menu.append(Gtk.SeparatorMenuItem()) menuitem = self.new_menu_item(_("Shrink Channels"), self.on_shrink_channels_cb, "minus") self.menu_item_add_accelerator(menuitem, "KP_Subtract") edit_menu.append(menuitem) menuitem = self.new_menu_item(_("Expand Channels"), self.on_expand_channels_cb, "plus") self.menu_item_add_accelerator(menuitem, "KP_Add") edit_menu.append(menuitem) edit_menu.append(Gtk.SeparatorMenuItem()) edit_menu.append(self.new_menu_item('Prefader Metering', self.on_prefader_meters_cb, "M")) edit_menu.append(self.new_menu_item('Postfader Metering', self.on_postfader_meters_cb, "M")) edit_menu.append(Gtk.SeparatorMenuItem()) edit_menu.append(self.new_menu_item(_("_Clear"), self.on_channels_clear, "X")) edit_menu.append(Gtk.SeparatorMenuItem()) self.preferences_dialog = None edit_menu.append( self.new_menu_item(_("_Preferences"), self.on_preferences_cb, "P") ) # Help menu help_menu = Gtk.Menu() help_menu_item.set_submenu(help_menu) help_menu.append(self.new_menu_item(_("_About"), self.on_about, "F1")) # Main panel self.hbox_top = Gtk.HBox() self.vbox_top.pack_start(self.hbox_top, True, True, 0) self.scrolled_window = Gtk.ScrolledWindow() self.scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.hbox_inputs = Gtk.Box() self.hbox_inputs.set_spacing(0) self.hbox_inputs.set_border_width(0) self.hbox_top.set_spacing(0) self.hbox_top.set_border_width(0) self.scrolled_window.add(self.hbox_inputs) self.hbox_outputs = Gtk.Box() self.hbox_outputs.set_spacing(0) self.hbox_outputs.set_border_width(0) self.scrolled_output = Gtk.ScrolledWindow() self.scrolled_output.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.scrolled_output.add(self.hbox_outputs) self.paned = Gtk.HPaned() self.paned.set_wide_handle(True) self.hbox_top.pack_start(self.paned, True, True, 0) self.paned.pack1(self.scrolled_window, True, False) self.paned.pack2(self.scrolled_output, True, False) self.window.connect("destroy", Gtk.main_quit) self.window.connect("delete-event", self.on_delete_event) # --------------------------------------------------------------------------------------------- # Channel creation def add_channel( self, name, stereo=True, direct_output=True, volume_cc=-1, balance_cc=-1, mute_cc=-1, solo_cc=-1, initial_vol=-1, ): try: channel = InputChannel( self, name, stereo=stereo, direct_output=direct_output, initial_vol=initial_vol ) self.add_channel_precreated(channel) except Exception: error_dialog(self.window, _("Input channel creation failed.")) return channel.assign_midi_ccs(volume_cc, balance_cc, mute_cc, solo_cc) return channel def add_channel_precreated(self, channel): frame = Gtk.Frame() frame.add(channel) self.hbox_inputs.pack_start(frame, False, True, 0) channel.realize() channel_edit_menu_item = Gtk.MenuItem(label=channel.channel_name) self.channel_edit_input_menu.append(channel_edit_menu_item) channel_edit_menu_item.connect("activate", self.on_edit_input_channel, channel) self.channel_edit_input_menu_item.set_sensitive(True) channel_remove_menu_item = Gtk.MenuItem(label=channel.channel_name) self.channel_remove_input_menu.append(channel_remove_menu_item) channel_remove_menu_item.connect("activate", self.on_remove_input_channel, channel) self.channel_remove_input_menu_item.set_sensitive(True) self.channels.append(channel) for outputchannel in self.output_channels: channel.add_control_group(outputchannel) if channel.wants_direct_output: self.add_direct_output(channel) channel.connect("input-channel-order-changed", self.on_input_channel_order_changed) def add_direct_output(self, channel, name=None): # Port names, which are not user-settable are not marked as translatable, # so they are the same regardless of the language setting of the environment # in which a project is loaded. if not name: name = "{channel_name} Out".format(channel_name=channel.channel_name) # create post fader output channel matching the input channel channel.post_fader_output_channel = self.mixer.add_output_channel( name, channel.channel.is_stereo, True ) channel.post_fader_output_channel.volume = 0 channel.post_fader_output_channel.set_solo(channel.channel, True) def add_output_channel( self, name, stereo=True, volume_cc=-1, balance_cc=-1, mute_cc=-1, display_solo_buttons=False, color="#fff", initial_vol=-1, ): try: channel = OutputChannel(self, name, stereo=stereo, initial_vol=initial_vol) channel.display_solo_buttons = display_solo_buttons channel.color = color self.add_output_channel_precreated(channel) except Exception: error_dialog(self.window, _("Output channel creation failed.")) return channel.assign_midi_ccs(volume_cc, balance_cc, mute_cc) return channel def add_output_channel_precreated(self, channel): frame = Gtk.Frame() frame.add(channel) self.hbox_outputs.pack_end(frame, False, True, 0) self.hbox_outputs.reorder_child(frame, 0) channel.realize() channel_edit_menu_item = Gtk.MenuItem(label=channel.channel_name) self.channel_edit_output_menu.append(channel_edit_menu_item) channel_edit_menu_item.connect("activate", self.on_edit_output_channel, channel) self.channel_edit_output_menu_item.set_sensitive(True) channel_remove_menu_item = Gtk.MenuItem(label=channel.channel_name) self.channel_remove_output_menu.append(channel_remove_menu_item) channel_remove_menu_item.connect("activate", self.on_remove_output_channel, channel) self.channel_remove_output_menu_item.set_sensitive(True) self.output_channels.append(channel) channel.connect("output-channel-order-changed", self.on_output_channel_order_changed) # --------------------------------------------------------------------------------------------- # Signal/event handlers # --------------------------------------------------------------------------------------------- # NSM def nsm_react(self): self.nsm_client.reactToMessage() return True def nsm_hide_cb(self, *args): self.window.hide() self.visible = False self.nsm_client.announceGuiVisibility(False) def nsm_show_cb(self): width, height = self.window.get_size() self.window.show_all() self.paned.set_position(self.paned_position / self.width * width) self.visible = True self.nsm_client.announceGuiVisibility(True) def nsm_open_cb(self, path, session_name, client_name): self.create_mixer(client_name, with_nsm=True) self.current_filename = path + ".xml" if isfile(self.current_filename): try: with open(self.current_filename, "r") as fp: self.load_from_xml(fp, from_nsm=True) except Exception as exc: # Re-raise with more meaningful error message raise IOError( _("Error loading project file '{filename}': {msg}").format( filename=self.current_filename, msg=exc ) ) def nsm_save_cb(self, path, session_name, client_name): self.current_filename = path + ".xml" with open(self.current_filename, "w") as fp: self.save_to_xml(fp) def nsm_exit_cb(self, path, session_name, client_name): Gtk.main_quit() # --------------------------------------------------------------------------------------------- # POSIX signals def sighandler(self, signum, frame): log.debug("Signal %d received.", signum) if signum == signal.SIGUSR1: GLib.timeout_add(0, self.on_save_cb) elif signum == signal.SIGINT or signum == signal.SIGTERM: GLib.timeout_add(0, self.on_quit_cb) else: log.warning("Unknown signal %d received.", signum) # --------------------------------------------------------------------------------------------- # GTK signals def on_language_changed(self, gui_factory, lang): global translation translation = gettext.translation( __program__, _localedir, languages=[lang] if lang else None, fallback=True ) translation.install() def on_about(self, *args): about = Gtk.AboutDialog() about.set_name(__program__) about.set_program_name(__program__) about.set_copyright( "Copyright © 2006-2021\n" "Nedko Arnaudov,\n" "Frédéric Péters, Arnout Engelen,\n" "Daniel Sheeler, Christopher Arndt" ) about.set_license(__license__) about.set_authors( [ "Nedko Arnaudov ", "Christopher Arndt ", "Arnout Engelen ", "John Hedges ", "Olivier Humbert ", "Sarah Mischke ", "Frédéric Péters ", "Daniel Sheeler ", "Athanasios Silis ", ] ) about.set_logo_icon_name(__program__) about.set_version(__version__) about.set_website("https://rdio.space/jackmixer/") about.run() about.destroy() def on_delete_event(self, widget, event): if self.nsm_client: self.nsm_hide_cb() return True return self.on_quit_cb(on_delete=True) def add_file_filters(self, dialog): filter_xml = Gtk.FileFilter() filter_xml.set_name(_("XML files")) filter_xml.add_mime_type("text/xml") dialog.add_filter(filter_xml) filter_all = Gtk.FileFilter() filter_all.set_name(_("All files")) filter_all.add_pattern("*") dialog.add_filter(filter_all) def _open_project(self, filename): try: with open(filename, "r") as fp: self.load_from_xml(fp) except Exception as exc: error_dialog( self.window, _("Error loading project file '{filename}': {msg}").format( filename=filename, msg=exc ), ) else: self.current_filename = filename return True def on_open_cb(self, *args): dlg = Gtk.FileChooserDialog( title=_("Open project"), parent=self.window, action=Gtk.FileChooserAction.OPEN ) dlg.add_buttons( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK ) dlg.set_default_response(Gtk.ResponseType.OK) default_project_path = self.gui_factory.get_default_project_path() if self.current_filename: dlg.set_current_folder(dirname(self.current_filename)) else: dlg.set_current_folder(self.last_project_path or default_project_path or os.getcwd()) if default_project_path: dlg.add_shortcut_folder(default_project_path) self.add_file_filters(dlg) if dlg.run() == Gtk.ResponseType.OK: filename = dlg.get_filename() if self._open_project(filename): self.recentmanager.add_item("file://" + abspath(filename)) dlg.destroy() def on_recent_file_chosen(self, recentchooser): item = recentchooser.get_current_item() if item and item.exists(): log.debug("Recent file menu entry selected: %s", item.get_display_name()) uri = item.get_uri() if not self._open_project(urlparse(uri).path): self.recentmanager.remove_item(uri) def _save_project(self, filename): with open(filename, "w") as fp: self.save_to_xml(fp) def on_save_cb(self, *args): if not self.current_filename: return self.on_save_as_cb() try: self._save_project(self.current_filename) except Exception as exc: error_dialog( self.window, _("Error saving project file '{filename}': {msg}").format( filename=self.current_filename, msg=exc ), ) def on_save_as_cb(self, *args): dlg = Gtk.FileChooserDialog( title=_("Save project"), parent=self.window, action=Gtk.FileChooserAction.SAVE ) dlg.add_buttons( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK ) dlg.set_default_response(Gtk.ResponseType.OK) dlg.set_do_overwrite_confirmation(True) default_project_path = self.gui_factory.get_default_project_path() if self.current_filename: dlg.set_filename(self.current_filename) else: dlg.set_current_folder(self.last_project_path or default_project_path or os.getcwd()) filename = "{}-{}.xml".format( getpass.getuser(), datetime.datetime.now().strftime("%Y%m%d-%H%M") ) dlg.set_current_name(filename) if default_project_path: dlg.add_shortcut_folder(default_project_path) self.add_file_filters(dlg) if dlg.run() == Gtk.ResponseType.OK: save_path = dlg.get_filename() save_dir = dirname(save_path) if isdir(save_dir): self.last_project_path = save_dir filename = dlg.get_filename() try: self._save_project(filename) except Exception as exc: error_dialog( self.window, _("Error saving project file '{filename}': {msg}").format( filename=filename, msg=exc ), ) else: self.current_filename = filename self.recentmanager.add_item("file://" + abspath(filename)) dlg.destroy() def on_quit_cb(self, *args, on_delete=False): if not self.nsm_client and self.gui_factory.get_confirm_quit(): dlg = Gtk.MessageDialog( parent=self.window, message_type=Gtk.MessageType.QUESTION, buttons=Gtk.ButtonsType.NONE, ) dlg.set_markup(_("Quit application?")) dlg.format_secondary_markup( _( "All jack_mixer ports will be closed and connections lost," "\nstopping all sound going through jack_mixer.\n\n" "Are you sure?" ) ) dlg.add_buttons( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_QUIT, Gtk.ResponseType.OK ) response = dlg.run() dlg.destroy() if response != Gtk.ResponseType.OK: return on_delete Gtk.main_quit() def on_shrink_channels_cb(self, widget): for channel in self.channels + self.output_channels: channel.narrow() def on_expand_channels_cb(self, widget): for channel in self.channels + self.output_channels: channel.widen() def on_prefader_meters_cb(self, widget): for channel in self.channels + self.output_channels: channel.use_prefader_metering() def on_postfader_meters_cb(self, widget): for channel in self.channels + self.output_channels: channel.use_prefader_metering(False) def on_midi_behavior_mode_changed(self, gui_factory, value): self.mixer.midi_behavior_mode = value def on_preferences_cb(self, widget): if not self.preferences_dialog: self.preferences_dialog = PreferencesDialog(self) self.preferences_dialog.show() self.preferences_dialog.present() def on_add_channel(self, inout="input", default_name="Input"): dialog = getattr(self, "_add_{}_dialog".format(inout), None) values = getattr(self, "_add_{}_values".format(inout), {}) if dialog is None: cls = NewInputChannelDialog if inout == "input" else NewOutputChannelDialog dialog = cls(app=self) setattr(self, "_add_{}_dialog".format(inout), dialog) names = { ch.channel_name for ch in (self.channels if inout == "input" else self.output_channels) } values.setdefault("name", default_name) while True: if values["name"] in names: values["name"] = add_number_suffix(values["name"]) else: break dialog.fill_ui(**values) dialog.set_transient_for(self.window) dialog.show() ret = dialog.run() dialog.hide() if ret == Gtk.ResponseType.OK: result = dialog.get_result() setattr(self, "_add_{}_values".format(inout), result) (self.add_channel if inout == "input" else self.add_output_channel)(**result) if self.visible or self.nsm_client is None: self.window.show_all() def on_add_input_channel(self, widget): return self.on_add_channel("input", _("Input")) def on_add_output_channel(self, widget): return self.on_add_channel("output", _("Output")) def on_edit_input_channel(self, widget, channel): log.debug('Editing input channel "%s".', channel.channel_name) channel.on_channel_properties() def on_remove_input_channel(self, widget, channel): log.debug('Removing input channel "%s".', channel.channel_name) def remove_channel_edit_input_menuitem_by_label(widget, label): if widget.get_label() == label: self.channel_edit_input_menu.remove(widget) self.channel_remove_input_menu.remove(widget) self.channel_edit_input_menu.foreach( remove_channel_edit_input_menuitem_by_label, channel.channel_name ) if self.monitored_channel is channel: channel.monitor_button.set_active(False) for i in range(len(self.channels)): if self.channels[i] is channel: channel.unrealize() del self.channels[i] self.hbox_inputs.remove(channel.get_parent()) break if not self.channels: self.channel_edit_input_menu_item.set_sensitive(False) self.channel_remove_input_menu_item.set_sensitive(False) def on_edit_output_channel(self, widget, channel): log.debug('Editing output channel "%s".', channel.channel_name) channel.on_channel_properties() def on_remove_output_channel(self, widget, channel): log.debug('Removing output channel "%s".', channel.channel_name) def remove_channel_edit_output_menuitem_by_label(widget, label): if widget.get_label() == label: self.channel_edit_output_menu.remove(widget) self.channel_remove_output_menu.remove(widget) self.channel_edit_output_menu.foreach( remove_channel_edit_output_menuitem_by_label, channel.channel_name ) if self.monitored_channel is channel: channel.monitor_button.set_active(False) for i in range(len(self.channels)): if self.output_channels[i] is channel: channel.unrealize() del self.output_channels[i] self.hbox_outputs.remove(channel.get_parent()) break if not self.output_channels: self.channel_edit_output_menu_item.set_sensitive(False) self.channel_remove_output_menu_item.set_sensitive(False) def on_channel_rename(self, oldname, newname): def rename_channels(container, parameters): if container.get_label() == parameters["oldname"]: container.set_label(parameters["newname"]) rename_parameters = {"oldname": oldname, "newname": newname} self.channel_edit_input_menu.foreach(rename_channels, rename_parameters) self.channel_edit_output_menu.foreach(rename_channels, rename_parameters) self.channel_remove_input_menu.foreach(rename_channels, rename_parameters) self.channel_remove_output_menu.foreach(rename_channels, rename_parameters) log.debug('Renaming channel from "%s" to "%s".', oldname, newname) def reorder_menu_item(self, menu, source_label, dest_label): pos = -1 source_item = None for i, menuitem in enumerate(menu.get_children()): label = menuitem.get_label() if label == source_label: source_item = menuitem elif label == dest_label: pos = i if pos != -1 and source_item is not None: menu.reorder_child(source_item, pos) def reorder_channels(self, container, source_name, dest_name, reverse=False): frames = container.get_children() for frame in frames: if source_name == frame.get_child().channel_name: source_frame = frame break if reverse: frames.reverse() for pos, frame in enumerate(frames): if dest_name == frame.get_child().channel_name: container.reorder_child(source_frame, pos) break def on_input_channel_order_changed(self, widget, source_name, dest_name): self.channels.clear() self.reorder_channels(self.hbox_inputs, source_name, dest_name) for frame in self.hbox_inputs.get_children(): self.channels.append(frame.get_child()) for menu in (self.channel_edit_input_menu, self.channel_remove_input_menu): self.reorder_menu_item(menu, source_name, dest_name) def on_output_channel_order_changed(self, widget, source_name, dest_name): self.output_channels.clear() self.reorder_channels(self.hbox_outputs, source_name, dest_name, reverse=True) for frame in self.hbox_outputs.get_children(): self.output_channels.append(frame.get_child()) for menu in (self.channel_edit_output_menu, self.channel_remove_output_menu): self.reorder_menu_item(menu, source_name, dest_name) def on_channels_clear(self, widget): dlg = Gtk.MessageDialog( parent=self.window, modal=True, message_type=Gtk.MessageType.WARNING, text=_("Are you sure you want to clear all channels?"), buttons=Gtk.ButtonsType.OK_CANCEL, ) if not widget or dlg.run() == Gtk.ResponseType.OK: for channel in self.output_channels: channel.unrealize() self.hbox_outputs.remove(channel.get_parent()) for channel in self.channels: channel.unrealize() self.hbox_inputs.remove(channel.get_parent()) self.channels = [] self.output_channels = [] self.channel_edit_input_menu = Gtk.Menu() self.channel_edit_input_menu_item.set_submenu(self.channel_edit_input_menu) self.channel_edit_input_menu_item.set_sensitive(False) self.channel_remove_input_menu = Gtk.Menu() self.channel_remove_input_menu_item.set_submenu(self.channel_remove_input_menu) self.channel_remove_input_menu_item.set_sensitive(False) self.channel_edit_output_menu = Gtk.Menu() self.channel_edit_output_menu_item.set_submenu(self.channel_edit_output_menu) self.channel_edit_output_menu_item.set_sensitive(False) self.channel_remove_output_menu = Gtk.Menu() self.channel_remove_output_menu_item.set_submenu(self.channel_remove_output_menu) self.channel_remove_output_menu_item.set_sensitive(False) # Force save-as dialog on next save self.current_filename = None dlg.destroy() def on_default_meter_scale_changed(self, sender, newscale): if isinstance(newscale, (scale.K14, scale.K20)): self.mixer.kmetering = True else: self.mixer.kmetering = False def on_meter_refresh_period_milliseconds_changed(self, sender, value): if self.meter_refresh_timer_id is not None: GLib.source_remove(self.meter_refresh_timer_id) self.meter_refresh_period = value self.meter_refresh_timer_id = GLib.timeout_add(self.meter_refresh_period, self.read_meters) def read_meters(self): for channel in self.channels: channel.read_meter() for channel in self.output_channels: channel.read_meter() return True def midi_events_check(self): for channel in self.channels + self.output_channels: channel.midi_events_check() return True def get_monitored_channel(self): return self._monitored_channel def set_monitored_channel(self, channel): if channel == self._monitored_channel: return self._monitored_channel = channel if channel is None: self.monitor_channel.out_mute = True elif isinstance(channel, InputChannel): # reset all solo/mute settings for in_channel in self.channels: self.monitor_channel.set_solo(in_channel.channel, False) self.monitor_channel.set_muted(in_channel.channel, False) self.monitor_channel.set_solo(channel.channel, True) self.monitor_channel.prefader = True self.monitor_channel.out_mute = False else: self.monitor_channel.prefader = False self.monitor_channel.out_mute = False if channel: self.update_monitor(channel) monitored_channel = property(get_monitored_channel, set_monitored_channel) def update_monitor(self, channel): if self._monitored_channel is not channel: return self.monitor_channel.volume = channel.channel.volume self.monitor_channel.balance = channel.channel.balance if isinstance(self.monitored_channel, OutputChannel): # sync solo/muted channels for input_channel in self.channels: self.monitor_channel.set_solo( input_channel.channel, channel.channel.is_solo(input_channel.channel) ) self.monitor_channel.set_muted( input_channel.channel, channel.channel.is_muted(input_channel.channel) ) def get_input_channel_by_name(self, name): for input_channel in self.channels: if input_channel.channel.name == name: return input_channel return None # --------------------------------------------------------------------------------------------- # Mixer project (de-)serialization and file handling def save_to_xml(self, file): log.debug("Saving to XML...") b = XmlSerialization() s = Serializator() s.serialize(self, b) b.save(file) def load_from_xml(self, file, silence_errors=False, from_nsm=False): log.debug("Loading from XML...") self.unserialized_channels = [] b = XmlSerialization() try: b.load(file, self.serialization_name()) except: # noqa: E722 if silence_errors: return raise self.on_channels_clear(None) s = Serializator() s.unserialize(self, b) for channel in self.unserialized_channels: if isinstance(channel, InputChannel): if self._init_solo_channels and channel.channel_name in self._init_solo_channels: channel.solo = True self.add_channel_precreated(channel) self._init_solo_channels = None for channel in self.unserialized_channels: if isinstance(channel, OutputChannel): self.add_output_channel_precreated(channel) del self.unserialized_channels width, height = self.window.get_size() if self.visible or not from_nsm: self.window.show_all() if self.output_channels: self.output_channels[-1].volume_digits.select_region(0, 0) self.output_channels[-1].slider.grab_focus() elif self.channels: self.channels[-1].volume_digits.select_region(0, 0) self.channels[-1].volume_digits.grab_focus() self.paned.set_position(self.paned_position / self.width * width) self.window.resize(self.width, self.height) def serialize(self, object_backend): width, height = self.window.get_size() object_backend.add_property("geometry", "%sx%s" % (width, height)) pos = self.paned.get_position() object_backend.add_property("paned_position", "%s" % pos) solo_channels = [] for input_channel in self.channels: if input_channel.channel.solo: solo_channels.append(input_channel) if solo_channels: object_backend.add_property( "solo_channels", "|".join([x.channel.name for x in solo_channels]) ) object_backend.add_property("visible", "%s" % str(self.visible)) def unserialize_property(self, name, value): if name == "geometry": width, height = value.split("x") self.width = int(width) self.height = int(height) return True elif name == "solo_channels": self._init_solo_channels = value.split("|") return True elif name == "visible": self.visible = value == "True" return True elif name == "paned_position": self.paned_position = int(value) return True return False def unserialize_child(self, name): if name == InputChannel.serialization_name(): channel = InputChannel(self, "", True) self.unserialized_channels.append(channel) return channel elif name == OutputChannel.serialization_name(): channel = OutputChannel(self, "", True) self.unserialized_channels.append(channel) return channel elif name == gui.Factory.serialization_name(): return self.gui_factory def serialization_get_childs(self): """Get child objects that require and support serialization.""" childs = self.channels[:] + self.output_channels[:] + [self.gui_factory] return childs def serialization_name(self): return __program__ # --------------------------------------------------------------------------------------------- # Main program loop def main(self): if not self.mixer: return if self.visible or self.nsm_client is None: width, height = self.window.get_size() self.window.show_all() if hasattr(self, "paned_position"): self.paned.set_position(self.paned_position / self.width * width) signal.signal(signal.SIGUSR1, self.sighandler) signal.signal(signal.SIGTERM, self.sighandler) signal.signal(signal.SIGINT, self.sighandler) signal.signal(signal.SIGHUP, signal.SIG_IGN) Gtk.main() def error_dialog(parent, msg, *args, **kw): if kw.get("debug"): log.exception(msg.format(*args)) err = Gtk.MessageDialog( parent=parent, modal=True, destroy_with_parent=True, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text=msg.format(*args), ) err.run() err.destroy() def main(): parser = argparse.ArgumentParser(prog=__program__, description=_(__doc__.splitlines()[0])) parser.add_argument( "-c", "--config", metavar=_("FILE"), help=_("load mixer project configuration from FILE") ) parser.add_argument( "-d", "--debug", action="store_true", default="JACK_MIXER_DEBUG" in os.environ, help=_("enable debug logging messages"), ) parser.add_argument( "client_name", metavar=_("NAME"), nargs="?", default=__program__, help=_("set JACK client name (default: %(default)s)"), ) args = parser.parse_args() logging.basicConfig( level=logging.DEBUG if args.debug else logging.INFO, format="%(levelname)s: %(message)s" ) try: mixer = JackMixer(args.client_name) except Exception as e: error_dialog(None, _("Mixer creation failed:\n\n{}"), e, debug=args.debug) sys.exit(1) if not mixer.nsm_client and args.config: try: with open(args.config) as fp: mixer.load_from_xml(fp) except Exception as exc: error_dialog( mixer.window, _("Error loading project file '{filename}': {msg}").format( filename=args.config, msg=exc ), ) else: mixer.current_filename = args.config mixer.window.set_default_size( 60 * (1 + len(mixer.channels) + len(mixer.output_channels)), 300 ) mixer.main() mixer.cleanup() if __name__ == "__main__": main() jack_mixer-release-17/jack_mixer/channel.py000066400000000000000000001664171413224161500211510ustar00rootroot00000000000000# This file is part of jack_mixer # # Copyright (C) 2006 Nedko Arnaudov # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. import logging import gi # noqa: F401 from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GObject from gi.repository import Pango from . import abspeak from . import meter from . import slider from .serialization import SerializedObject from .styling import set_background_color, random_color log = logging.getLogger(__name__) BUTTON_PADDING = 1 class Channel(Gtk.Box, SerializedObject): """Widget with slider and meter used as base class for more specific channel widgets""" num_instances = 0 def __init__(self, app, name, stereo=True, direct_output=True, initial_vol=None): super().__init__(orientation=Gtk.Orientation.VERTICAL) self.app = app self.mixer = app.mixer self.channel = None self.gui_factory = app.gui_factory self._channel_name = name self.stereo = stereo self.initial_vol = initial_vol self.meter_scale = self.gui_factory.get_default_meter_scale() self.slider_scale = self.gui_factory.get_default_slider_scale() self.slider_adjustment = slider.AdjustmentdBFS(self.slider_scale, 0.0, 0.02) self.balance_adjustment = slider.BalanceAdjustment() self.wants_direct_output = direct_output self.post_fader_output_channel = None self.future_out_mute = None self.future_volume_midi_cc = None self.future_balance_midi_cc = None self.future_mute_midi_cc = None self.future_solo_midi_cc = None self.css_name = "css_name_%d" % Channel.num_instances self.label_name = None self.wide = True self.meter_prefader = False self.prefader_button = None self.label_chars_wide = 12 self.label_chars_narrow = 7 self.channel_properties_dialog = None self.monitor_button = None Channel.num_instances += 1 # --------------------------------------------------------------------------------------------- # Properties @property def channel_name(self): return self._channel_name @channel_name.setter def channel_name(self, name): self.app.on_channel_rename(self._channel_name, name) self._channel_name = name if self.label_name: self.label_name.set_text(name) if len(name) > (self.label_chars_wide if self.wide else self.label_chars_narrow): self.label_name.set_tooltip_text(name) if self.channel: self.channel.name = name if self.post_fader_output_channel: self.post_fader_output_channel.name = "{channel_name} Out".format(channel_name=name) # --------------------------------------------------------------------------------------------- # UI creation and (de-)initialization def create_balance_widget(self): parent = None if self.balance: parent = self.balance.get_parent() self.balance.destroy() self.balance = slider.BalanceSlider(self.balance_adjustment, (20, 20), (0, 100)) if parent: parent.pack_end(self.balance, False, True, 0) self.balance.show() def create_buttons(self): # Mute, Solo and Monitor buttons self.hbox_mutesolo = Gtk.Box(False, 0, orientation=Gtk.Orientation.HORIZONTAL) self.mute = Gtk.ToggleButton() self.mute.set_label(_("M")) self.mute.get_style_context().add_class("mute") self.mute.set_active(self.channel.out_mute) self.mute.connect("toggled", self.on_mute_toggled) self.mute.connect("button-press-event", self.on_mute_button_pressed) self.hbox_mutesolo.pack_start(self.mute, True, True, 0) self.pack_start(self.hbox_mutesolo, False, False, 0) self.monitor_button = Gtk.ToggleButton(_("MON")) self.monitor_button.get_style_context().add_class("monitor") self.monitor_button.connect("toggled", self.on_monitor_button_toggled) self.pack_start(self.monitor_button, False, False, 0) def create_fader(self): # HBox for fader and meter self.vbox_fader = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.vbox_fader.get_style_context().add_class("vbox_fader") self.prefader_button = pre = Gtk.ToggleButton(_("PRE")) pre.get_style_context().add_class("prefader_meter") pre.set_tooltip_text(_("Pre-fader (on) / Post-fader (off) metering")) pre.connect("toggled", self.on_prefader_metering_toggled) pre.set_active(self.meter_prefader) self.hbox_readouts = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.hbox_readouts.set_homogeneous(True) self.hbox_readouts.pack_start(self.volume_digits, False, True, 0) self.hbox_readouts.pack_start(self.abspeak, False, True, 0) self.vbox_fader.pack_start(self.hbox_readouts, False, False, 0) self.hbox_fader = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.hbox_fader.pack_start(self.slider, True, False, 0) self.vbox_meter = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.vbox_meter.pack_start(self.meter, True, True, 0) self.vbox_meter.pack_start(self.prefader_button, False, True, 0) self.hbox_fader.pack_start(self.vbox_meter, True, True, 0) self.event_box_fader = Gtk.EventBox() self.event_box_fader.set_events(Gdk.EventMask.SCROLL_MASK) self.event_box_fader.connect("scroll-event", self.on_scroll) self.event_box_fader.add(self.hbox_fader) self.vbox_fader.pack_start(self.event_box_fader, True, True, 0) self.vbox_fader.pack_end(self.balance, False, True, 0) self.pack_start(self.vbox_fader, True, True, 0) def create_slider_widget(self): parent = None if self.slider: parent = self.slider.get_parent() self.slider.destroy() if self.gui_factory.use_custom_widgets: self.slider = slider.CustomSliderWidget(self.slider_adjustment) else: self.slider = slider.VolumeSlider(self.slider_adjustment) if parent: parent.pack_start(self.slider, True, False, 0) parent.reorder_child(self.slider, 0) self.slider.widen(self.wide) self.slider.show() def realize(self): log.debug('Realizing channel "%s".', self.channel_name) if self.future_out_mute is not None: self.channel.out_mute = self.future_out_mute # Widgets # Channel strip label self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.pack_start(self.vbox, False, True, 0) self.label_name = Gtk.Label() self.label_name.get_style_context().add_class("top_label") self.label_name.set_text(self.channel_name) self.label_name.set_max_width_chars( self.label_chars_wide if self.wide else self.label_chars_narrow ) self.label_name.set_ellipsize(Pango.EllipsizeMode.MIDDLE) self.label_name_event_box = Gtk.EventBox() self.label_name_event_box.connect("button-press-event", self.on_label_mouse) self.label_name_event_box.add(self.label_name) # Volume slider self.slider = None self.create_slider_widget() # Balance slider self.balance = None self.create_balance_widget() # Volume entry self.volume_digits = Gtk.Entry() self.volume_digits.set_has_frame(False) self.volume_digits.set_width_chars(5) self.volume_digits.set_property("xalign", 0.5) self.volume_digits.connect("key-press-event", self.on_volume_digits_key_pressed) self.volume_digits.connect("focus-out-event", self.on_volume_digits_focus_out) self.volume_digits.get_style_context().add_class("readout") # Peak level label self.abspeak = abspeak.AbspeakWidget(self.app) self.abspeak.connect("reset", self.on_abspeak_reset) self.abspeak.connect("volume-adjust", self.on_abspeak_adjust) self.abspeak.get_style_context().add_class("readout") # Level meter if self.stereo: self.meter = meter.StereoMeterWidget(self.meter_scale) else: self.meter = meter.MonoMeterWidget(self.meter_scale) self.on_vumeter_color_changed(self.gui_factory) # If channel is created via UI, the initial volume is passed to the # init method and saved in the `initial_vol` attribute. # If channel is created from a project XML file, no initial volume is # passsed to init, `initial_vol` will be `None` and the volume slider # adjustment is set via the `unserialize_property` method. # In both cases the engine volume (and balance) will be synchronized # with the slider adjustment by the overwritten `realize` method in # Channel sub-classes. if self.initial_vol is not None: if self.initial_vol == -1: self.slider_adjustment.set_value(0) else: self.slider_adjustment.set_value_db(self.initial_vol) self.slider_adjustment.connect("volume-changed", self.on_volume_changed) self.slider_adjustment.connect( "volume-changed-from-midi", self.on_volume_changed_from_midi ) self.balance_adjustment.connect("balance-changed", self.on_balance_changed) self.gui_factory.connect( "default-meter-scale-changed", self.on_default_meter_scale_changed ) self.gui_factory.connect( "default-slider-scale-changed", self.on_default_slider_scale_changed ) self.gui_factory.connect("vumeter-color-changed", self.on_vumeter_color_changed) self.gui_factory.connect("vumeter-color-scheme-changed", self.on_vumeter_color_changed) self.gui_factory.connect("use-custom-widgets-changed", self.on_custom_widgets_changed) self.connect("key-press-event", self.on_key_pressed) self.connect("scroll-event", self.on_scroll) entries = [Gtk.TargetEntry.new(self.__class__.__name__, Gtk.TargetFlags.SAME_APP, 0)] self.label_name_event_box.drag_source_set( Gdk.ModifierType.BUTTON1_MASK, entries, Gdk.DragAction.MOVE ) self.label_name_event_box.connect("drag-data-get", self.on_drag_data_get) self.drag_dest_set(Gtk.DestDefaults.ALL, entries, Gdk.DragAction.MOVE) self.connect_after("drag-data-received", self.on_drag_data_received) self.vbox.pack_start(self.label_name_event_box, True, True, 0) def unrealize(self): log.debug('Unrealizing channel "%s".', self.channel_name) # --------------------------------------------------------------------------------------------- # Signal/event handlers def on_label_mouse(self, widget, event): if event.type == Gdk.EventType._2BUTTON_PRESS: if event.button == 1: self.on_channel_properties() return True elif ( event.state & Gdk.ModifierType.CONTROL_MASK and event.type == Gdk.EventType.BUTTON_PRESS and event.button == 1 ): if self.wide: self.narrow() else: self.widen() return True def on_channel_properties(self): if not self.channel_properties_dialog: self.channel_properties_dialog = self.properties_dialog_class(self.app, self) self.channel_properties_dialog.fill_and_show() def on_default_meter_scale_changed(self, gui_factory, scale): log.debug("Default meter scale change detected.") self.meter.set_scale(scale) self.meter_scale = scale def on_default_slider_scale_changed(self, gui_factory, scale): log.debug("Default slider scale change detected.") self.slider_scale = scale self.slider_adjustment.set_scale(scale) if self.channel: self.channel.midi_scale = self.slider_scale.scale def on_vumeter_color_changed(self, gui_factory, *args): color = gui_factory.get_vumeter_color() color_scheme = gui_factory.get_vumeter_color_scheme() if color_scheme != "solid": self.meter.set_color(None) else: self.meter.set_color(Gdk.color_parse(color)) def on_custom_widgets_changed(self, gui_factory, value): self.create_slider_widget() # balance slider has no custom variant, no need to re-create it. def on_prefader_metering_toggled(self, button): self.use_prefader_metering(button.get_active()) def on_abspeak_adjust(self, abspeak, adjust): log.debug("abspeak adjust %f", adjust) self.slider_adjustment.set_value_db(self.slider_adjustment.get_value_db() + adjust) if self.meter_prefader: self.channel.abspeak_prefader = None else: self.channel.abspeak_postfader = None # We want to update gui even if actual decibels have not changed (scale wrap for example) # self.update_volume(False) def on_abspeak_reset(self, abspeak): log.debug("abspeak reset") if self.meter_prefader: self.channel.abspeak_prefader = None else: self.channel.abspeak_postfader = None def on_volume_digits_key_pressed(self, widget, event): if event.keyval == Gdk.KEY_Return or event.keyval == Gdk.KEY_KP_Enter: db_text = self.volume_digits.get_text() try: db = float(db_text) log.debug("Volume digits confirmation '%f dBFS'.", db) except ValueError: log.debug("Volume digits confirmation ignore, reset to current.") self.update_volume(False) return self.slider_adjustment.set_value_db(db) # self.grab_focus() # We want to update gui even if actual decibels have not changed # (scale wrap for example) # self.update_volume(False) def on_volume_digits_focus_out(self, widget, event): log.debug("Volume digits focus out detected.") self.update_volume(False) def on_scroll(self, widget, event): if event.direction == Gdk.ScrollDirection.DOWN: self.slider_adjustment.step_down() elif event.direction == Gdk.ScrollDirection.UP: self.slider_adjustment.step_up() return True def on_volume_changed(self, adjustment): self.update_volume(True) def on_volume_changed_from_midi(self, adjustment): self.update_volume(True, from_midi=True) def on_balance_changed(self, adjustment): self.update_balance(True) def on_key_pressed(self, widget, event): if event.keyval == Gdk.KEY_Up: log.debug(self.channel_name + " Up") self.slider_adjustment.step_up() return True elif event.keyval == Gdk.KEY_Down: log.debug(self.channel_name + " Down") self.slider_adjustment.step_down() return True return False def on_drag_data_get(self, widget, drag_context, data, info, time): data.set(data.get_target(), 8, self.channel_name.encode("utf-8")) def on_drag_data_received(self, widget, drag_context, x, y, data, info, time): pass def on_midi_event_received(self, *args): self.slider_adjustment.set_value_db(self.channel.volume, from_midi=True) self.balance_adjustment.set_balance(self.channel.balance, from_midi=True) def on_mute_toggled(self, button): self.channel.out_mute = self.mute.get_active() def on_mute_button_pressed(self, button, event, *args): # should be overwritten by sub-class pass def on_monitor_button_toggled(self, button): if button.get_active(): for channel in self.app.channels + self.app.output_channels: if channel is not self and channel.monitor_button.get_active(): channel.monitor_button.handler_block_by_func(channel.on_monitor_button_toggled) channel.monitor_button.set_active(False) channel.monitor_button.handler_unblock_by_func( channel.on_monitor_button_toggled ) self.app.monitored_channel = self else: if self.app.monitored_channel is self: self.app.monitored_channel = None def assign_midi_ccs(self, volume_cc, balance_cc, mute_cc, solo_cc=None): try: if volume_cc != -1: self.channel.volume_midi_cc = volume_cc else: volume_cc = self.channel.autoset_volume_midi_cc() log.debug("Channel '%s' volume assigned to CC #%s.", self.channel.name, volume_cc) except Exception as exc: log.error("Channel '%s' volume CC assignment failed: %s", self.channel.name, exc) try: if balance_cc != -1: self.channel.balance_midi_cc = balance_cc else: balance_cc = self.channel.autoset_balance_midi_cc() log.debug("Channel '%s' balance assigned to CC #%s.", self.channel.name, balance_cc) except Exception as exc: log.error("Channel '%s' balance CC assignment failed: %s", self.channel.name, exc) try: if mute_cc != -1: self.channel.mute_midi_cc = mute_cc else: mute_cc = self.channel.autoset_mute_midi_cc() log.debug("Channel '%s' mute assigned to CC #%s.", self.channel.name, mute_cc) except Exception as exc: log.error("Channel '%s' mute CC assignment failed: %s", self.channel.name, exc) if solo_cc is not None: try: if solo_cc != -1: self.channel.solo_midi_cc = solo_cc else: solo_cc = self.channel.autoset_solo_midi_cc() log.debug("Channel '%s' solo assigned to CC #%s.", self.channel.name, solo_cc) except Exception as exc: log.error("Channel '%s' solo CC assignment failed: %s", self.channel.name, exc) # --------------------------------------------------------------------------------------------- # Channel operations def set_monitored(self): if self.channel: self.app.set_monitored_channel(self) self.monitor_button.set_active(True) def set_color(self, color): self.color = color set_background_color(self.label_name_event_box, self.css_name, self.color) def widen(self, flag=True): self.wide = flag ctx = self.label_name.get_style_context() if flag: ctx.remove_class("narrow") ctx.add_class("wide") else: ctx.remove_class("wide") ctx.add_class("narrow") label = self.label_name.get_label() label_width = self.label_chars_wide if flag else self.label_chars_narrow self.label_name.set_max_width_chars(label_width) if len(label) > label_width: self.label_name.set_tooltip_text(label) self.slider.widen(flag) self.meter.widen(flag) self.hbox_readouts.set_orientation( Gtk.Orientation.HORIZONTAL if flag else Gtk.Orientation.VERTICAL ) def narrow(self): self.widen(False) def use_prefader_metering(self, flag=True): self.meter_prefader = flag self.prefader_button.set_active(flag) if (self.meter.kmetering): # Reset the kmeter rms self.channel.kmeter_reset() def read_meter(self): if not self.channel: return if self.stereo: if self.meter.kmetering: if self.meter_prefader: self.meter.set_values_kmeter(*self.channel.kmeter_prefader) else: self.meter.set_values_kmeter(*self.channel.kmeter_postfader) else: if self.meter_prefader: self.meter.set_values(*self.channel.meter_prefader) else: self.meter.set_values(*self.channel.meter_postfader) else: if self.meter.kmetering: if self.meter_prefader: self.meter.set_value_kmeter(*self.channel.kmeter_prefader) else: self.meter.set_value_kmeter(*self.channel.kmeter_postfader) else: if self.meter_prefader: self.meter.set_value(*self.channel.meter_prefader) else: self.meter.set_value(*self.channel.meter_postfader) if self.meter_prefader: self.abspeak.set_peak(self.channel.abspeak_prefader) else: self.abspeak.set_peak(self.channel.abspeak_postfader) def update_balance(self, update_engine, from_midi=False): balance = self.balance_adjustment.get_value() log.debug("%s balance: %f", self.channel_name, balance) if update_engine: if not from_midi: self.channel.balance = balance self.channel.set_midi_cc_balance_picked_up(False) self.app.update_monitor(self) def update_volume(self, update_engine, from_midi=False): db = self.slider_adjustment.get_value_db() log.debug("%s volume: %.2f dB", self.channel_name, db) # TODO l10n db_text = "%.2f" % db self.volume_digits.set_text(db_text) if update_engine: if not from_midi: self.channel.volume = db self.channel.set_midi_cc_volume_picked_up(False) self.app.update_monitor(self) # --------------------------------------------------------------------------------------------- # Channel (de-)serialization def serialize(self, object_backend): object_backend.add_property("volume", "%f" % self.slider_adjustment.get_value_db()) object_backend.add_property("balance", "%f" % self.balance_adjustment.get_value()) object_backend.add_property("wide", "%s" % str(self.wide)) object_backend.add_property("meter_prefader", "%s" % str(self.meter_prefader)) if hasattr(self.channel, "out_mute"): object_backend.add_property("out_mute", str(self.channel.out_mute)) if self.channel.volume_midi_cc != -1: object_backend.add_property("volume_midi_cc", str(self.channel.volume_midi_cc)) if self.channel.balance_midi_cc != -1: object_backend.add_property("balance_midi_cc", str(self.channel.balance_midi_cc)) if self.channel.mute_midi_cc != -1: object_backend.add_property("mute_midi_cc", str(self.channel.mute_midi_cc)) if self.channel.solo_midi_cc != -1: object_backend.add_property("solo_midi_cc", str(self.channel.solo_midi_cc)) def unserialize_property(self, name, value): if name == "volume": self.slider_adjustment.set_value_db(float(value)) return True elif name == "balance": self.balance_adjustment.set_value(float(value)) return True elif name == "out_mute": self.future_out_mute = value == "True" return True elif name == "meter_prefader": self.meter_prefader = (value == "True") return True elif name == "volume_midi_cc": self.future_volume_midi_cc = int(value) return True elif name == "balance_midi_cc": self.future_balance_midi_cc = int(value) return True elif name == "mute_midi_cc": self.future_mute_midi_cc = int(value) return True elif name == "solo_midi_cc": self.future_solo_midi_cc = int(value) return True elif name == "wide": self.wide = value == "True" return True return False class InputChannel(Channel): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.properties_dialog_class = ChannelPropertiesDialog def create_buttons(self): super().create_buttons() self.solo = Gtk.ToggleButton() self.solo.set_label(_("S")) self.solo.get_style_context().add_class("solo") self.solo.set_active(self.channel.solo) self.solo.connect("toggled", self.on_solo_toggled) self.solo.connect("button-press-event", self.on_solo_button_pressed) self.hbox_mutesolo.pack_start(self.solo, True, True, 0) def realize(self): self.channel = self.mixer.add_channel(self.channel_name, self.stereo) if self.channel is None: raise Exception(_("Cannot create a channel")) super().realize() if self.future_volume_midi_cc is not None: self.channel.volume_midi_cc = self.future_volume_midi_cc if self.future_balance_midi_cc is not None: self.channel.balance_midi_cc = self.future_balance_midi_cc if self.future_mute_midi_cc is not None: self.channel.mute_midi_cc = self.future_mute_midi_cc if self.future_solo_midi_cc is not None: self.channel.solo_midi_cc = self.future_solo_midi_cc if self.app._init_solo_channels and self.channel_name in self.app._init_solo_channels: self.channel.solo = True self.channel.midi_scale = self.slider_scale.scale self.update_volume(update_engine=True) self.update_balance(update_engine=True) self.create_fader() # Widgets self.create_buttons() if not self.wide: self.narrow() def unrealize(self): super().unrealize() if self.post_fader_output_channel: self.post_fader_output_channel.remove() self.post_fader_output_channel = None self.channel.remove() self.channel = None def narrow(self): super().narrow() for cg in self.get_control_groups(): cg.narrow() def widen(self, flag=True): super().widen(flag) for cg in self.get_control_groups(): cg.widen() def on_drag_data_received(self, widget, drag_context, x, y, data, info, time): source_name = data.get_data().decode("utf-8") if source_name == self.channel_name: return self.emit("input-channel-order-changed", source_name, self.channel_name) def add_control_group(self, channel): control_group = ControlGroup(channel, self) control_group.show_all() self.vbox.pack_start(control_group, True, True, 0) return control_group def remove_control_group(self, channel): ctlgroup = self.get_control_group(channel) self.vbox.remove(ctlgroup) def update_control_group(self, channel): for control_group in self.vbox.get_children(): if isinstance(control_group, ControlGroup): if control_group.output_channel is channel: control_group.update() def get_control_group(self, channel): for control_group in self.get_control_groups(): if control_group.output_channel is channel: return control_group return None def get_control_groups(self): ctlgroups = [] for c in self.vbox.get_children(): if isinstance(c, ControlGroup): ctlgroups.append(c) return ctlgroups def midi_events_check(self): if self.channel is not None and self.channel.midi_in_got_events: self.mute.set_active(self.channel.out_mute) self.solo.set_active(self.channel.solo) super().on_midi_event_received() def on_mute_button_pressed(self, button, event, *args): if event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK: # Ctrlr+left-click: exclusive (between input channels) mute for channel in self.app.channels: if channel is not self: channel.mute.set_active(False) return button.get_active() elif event.button == 3: # Right-click on the mute button: act on all output channels if button.get_active(): # was muted button.set_active(False) if hasattr(button, "touched_channels"): touched_channels = button.touched_channels for chan in touched_channels: ctlgroup = self.get_control_group(chan) ctlgroup.mute.set_active(False) del button.touched_channels else: # was not muted button.set_active(True) touched_channels = [] for chan in self.app.output_channels: ctlgroup = self.get_control_group(chan) if not ctlgroup.mute.get_active(): ctlgroup.mute.set_active(True) touched_channels.append(chan) button.touched_channels = touched_channels return True return False def on_solo_toggled(self, button): self.channel.solo = self.solo.get_active() def on_solo_button_pressed(self, button, event, *args): if event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK: # Ctrlr+left-click: exclusive solo for channel in self.app.channels: if channel is not self: channel.solo.set_active(False) return button.get_active() elif event.button == 3: # Right click on the solo button: act on all output channels if button.get_active(): # was soloed button.set_active(False) if hasattr(button, "touched_channels"): touched_channels = button.touched_channels for chan in touched_channels: ctlgroup = self.get_control_group(chan) ctlgroup.solo.set_active(False) del button.touched_channels else: # was not soloed button.set_active(True) touched_channels = [] for chan in self.app.output_channels: ctlgroup = self.get_control_group(chan) if not ctlgroup.solo.get_active(): ctlgroup.solo.set_active(True) touched_channels.append(chan) button.touched_channels = touched_channels return True return False @classmethod def serialization_name(cls): return "input_channel" def serialize(self, object_backend): object_backend.add_property("name", self.channel_name) if self.stereo: object_backend.add_property("type", "stereo") else: object_backend.add_property("type", "mono") object_backend.add_property("direct_output", "%s" % str(self.wants_direct_output)) super().serialize(object_backend) def unserialize_property(self, name, value): if name == "name": self.channel_name = str(value) return True elif name == "type": if value == "stereo": self.stereo = True return True elif value == "mono": self.stereo = False return True elif name == "direct_output": self.wants_direct_output = value == "True" return True return super().unserialize_property(name, value) class OutputChannel(Channel): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.properties_dialog_class = OutputChannelPropertiesDialog self._display_solo_buttons = False self._init_muted_channels = None self._init_solo_channels = None self._init_prefader_channels = None self._color = Gdk.RGBA() @property def display_solo_buttons(self): return self._display_solo_buttons @display_solo_buttons.setter def display_solo_buttons(self, value): self._display_solo_buttons = value # notifying control groups for inputchannel in self.app.channels: inputchannel.update_control_group(self) @property def color(self): return self._color @color.setter def color(self, value): if isinstance(value, Gdk.RGBA): self._color = value else: c = Gdk.RGBA() c.parse(value) self._color = c def realize(self): self.channel = self.mixer.add_output_channel(self.channel_name, self.stereo) if self.channel is None: raise Exception(_("Cannot create an output channel")) super().realize() if self.future_volume_midi_cc is not None: self.channel.volume_midi_cc = self.future_volume_midi_cc if self.future_balance_midi_cc is not None: self.channel.balance_midi_cc = self.future_balance_midi_cc if self.future_mute_midi_cc is not None: self.channel.mute_midi_cc = self.future_mute_midi_cc self.channel.midi_scale = self.slider_scale.scale self.update_volume(update_engine=True) self.update_balance(update_engine=True) set_background_color(self.label_name_event_box, self.css_name, self.color) self.create_fader() # Widgets self.create_buttons() # add control groups to the input channels, and initialize them # appropriately for input_channel in self.app.channels: ctlgroup = input_channel.add_control_group(self) name = input_channel.channel.name if self._init_muted_channels and name in self._init_muted_channels: ctlgroup.mute.set_active(True) if self._init_solo_channels and name in self._init_solo_channels: ctlgroup.solo.set_active(True) if self._init_prefader_channels and name in self._init_prefader_channels: ctlgroup.prefader.set_active(True) if not input_channel.wide: ctlgroup.narrow() self._init_muted_channels = None self._init_solo_channels = None self._init_prefader_channels = None if not self.wide: self.narrow() def unrealize(self): # remove control groups from input channels for input_channel in self.app.channels: input_channel.remove_control_group(self) # then remove itself super().unrealize() self.channel.remove() self.channel = None def on_drag_data_received(self, widget, drag_context, x, y, data, info, time): source_name = data.get_data().decode("utf-8") if source_name == self.channel_name: return self.emit("output-channel-order-changed", source_name, self.channel_name) def on_mute_button_pressed(self, button, event, *args): if event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK: # Ctrlr+left-click: exclusive (between output channels) mute for channel in self.app.output_channels: if channel is not self: channel.mute.set_active(False) return button.get_active() def midi_events_check(self): if self.channel is not None and self.channel.midi_in_got_events: self.mute.set_active(self.channel.out_mute) super().on_midi_event_received() @classmethod def serialization_name(cls): return "output_channel" def serialize(self, object_backend): object_backend.add_property("name", self.channel_name) if self.stereo: object_backend.add_property("type", "stereo") else: object_backend.add_property("type", "mono") if self.display_solo_buttons: object_backend.add_property("solo_buttons", "true") muted_channels = [] solo_channels = [] prefader_in_channels = [] for input_channel in self.app.channels: if self.channel.is_muted(input_channel.channel): muted_channels.append(input_channel) if self.channel.is_solo(input_channel.channel): solo_channels.append(input_channel) if self.channel.is_in_prefader(input_channel.channel): prefader_in_channels.append(input_channel) if muted_channels: object_backend.add_property( "muted_channels", "|".join([x.channel.name for x in muted_channels]) ) if solo_channels: object_backend.add_property( "solo_channels", "|".join([x.channel.name for x in solo_channels]) ) if prefader_in_channels: object_backend.add_property( "prefader_channels", "|".join([x.channel.name for x in prefader_in_channels]) ) object_backend.add_property("color", self.color.to_string()) super().serialize(object_backend) def unserialize_property(self, name, value): if name == "name": self.channel_name = str(value) return True elif name == "type": if value == "stereo": self.stereo = True return True elif value == "mono": self.stereo = False return True elif name == "solo_buttons": if value == "true": self.display_solo_buttons = True return True elif name == "muted_channels": self._init_muted_channels = value.split("|") return True elif name == "solo_channels": self._init_solo_channels = value.split("|") return True elif name == "prefader_channels": self._init_prefader_channels = value.split("|") return True elif name == "color": c = Gdk.RGBA() c.parse(value) self.color = c return True return super().unserialize_property(name, value) class ChannelPropertiesDialog(Gtk.Dialog): def __init__(self, app, channel=None, title=None): if not title: if not channel: raise ValueError("Either 'title' or 'channel' must be passed.") title = _("Channel '{name}' Properties").format(name=channel.channel_name) super().__init__(title, app.window) self.channel = channel self.app = app self.mixer = app.mixer self.set_default_size(365, -1) self.create_ui() def fill_and_show(self): self.fill_ui() self.present() def add_buttons(self): self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) self.ok_button = self.add_button(Gtk.STOCK_APPLY, Gtk.ResponseType.APPLY) self.set_default_response(Gtk.ResponseType.APPLY) self.connect("response", self.on_response_cb) self.connect("delete-event", self.on_response_cb) def create_frame(self, label, child, padding=8): # need to pass an empty label, otherwise no label widget is created frame = Gtk.Frame(label="") frame.get_label_widget().set_markup("%s" % label) frame.set_border_width(3) frame.set_shadow_type(Gtk.ShadowType.NONE) alignment = Gtk.Alignment.new(0.5, 0, 1, 1) alignment.set_padding(padding, padding, padding, padding) frame.add(alignment) alignment.add(child) return frame def create_ui(self): self.add_buttons() vbox = self.get_content_area() self.properties_grid = grid = Gtk.Grid() vbox.pack_start(self.create_frame(_("Properties"), grid), True, True, 0) grid.set_row_spacing(8) grid.set_column_spacing(8) grid.set_column_homogeneous(True) name_label = Gtk.Label.new_with_mnemonic(_("_Name")) name_label.set_halign(Gtk.Align.START) grid.attach(name_label, 0, 0, 1, 1) self.entry_name = Gtk.Entry() self.entry_name.set_activates_default(True) self.entry_name.connect("changed", self.on_entry_name_changed) name_label.set_mnemonic_widget(self.entry_name) grid.attach(self.entry_name, 1, 0, 2, 1) grid.attach(Gtk.Label(label=_("Mode"), halign=Gtk.Align.START), 0, 1, 1, 1) self.mono = Gtk.RadioButton.new_with_mnemonic(None, _("_Mono")) self.stereo = Gtk.RadioButton.new_with_mnemonic_from_widget(self.mono, _("_Stereo")) grid.attach(self.mono, 1, 1, 1, 1) grid.attach(self.stereo, 2, 1, 1, 1) grid = Gtk.Grid() vbox.pack_start(self.create_frame(_("MIDI Control Changes"), grid), True, True, 0) grid.set_row_spacing(8) grid.set_column_spacing(8) grid.set_column_homogeneous(True) cc_tooltip = _( "{param} MIDI Control Change number " "(0-127, set to -1 to assign next free CC #)" ) volume_label = Gtk.Label.new_with_mnemonic(_("_Volume")) volume_label.set_halign(Gtk.Align.START) grid.attach(volume_label, 0, 0, 1, 1) self.entry_volume_cc = Gtk.SpinButton.new_with_range(-1, 127, 1) self.entry_volume_cc.set_tooltip_text(cc_tooltip.format(param=_("Volume"))) volume_label.set_mnemonic_widget(self.entry_volume_cc) grid.attach(self.entry_volume_cc, 1, 0, 1, 1) self.button_sense_midi_volume = Gtk.Button(_("Learn")) self.button_sense_midi_volume.connect("clicked", self.on_sense_midi_volume_clicked) grid.attach(self.button_sense_midi_volume, 2, 0, 1, 1) balance_label = Gtk.Label.new_with_mnemonic(_("_Balance")) balance_label.set_halign(Gtk.Align.START) grid.attach(balance_label, 0, 1, 1, 1) self.entry_balance_cc = Gtk.SpinButton.new_with_range(-1, 127, 1) self.entry_balance_cc.set_tooltip_text(cc_tooltip.format(param=_("Balance"))) balance_label.set_mnemonic_widget(self.entry_balance_cc) grid.attach(self.entry_balance_cc, 1, 1, 1, 1) self.button_sense_midi_balance = Gtk.Button(_("Learn")) self.button_sense_midi_balance.connect("clicked", self.on_sense_midi_balance_clicked) grid.attach(self.button_sense_midi_balance, 2, 1, 1, 1) mute_label = Gtk.Label.new_with_mnemonic(_("M_ute")) mute_label.set_halign(Gtk.Align.START) grid.attach(mute_label, 0, 2, 1, 1) self.entry_mute_cc = Gtk.SpinButton.new_with_range(-1, 127, 1) self.entry_mute_cc.set_tooltip_text(cc_tooltip.format(param=_("Mute"))) mute_label.set_mnemonic_widget(self.entry_mute_cc) grid.attach(self.entry_mute_cc, 1, 2, 1, 1) self.button_sense_midi_mute = Gtk.Button(_("Learn")) self.button_sense_midi_mute.connect("clicked", self.on_sense_midi_mute_clicked) grid.attach(self.button_sense_midi_mute, 2, 2, 1, 1) if isinstance(self, NewInputChannelDialog) or ( self.channel and isinstance(self.channel, InputChannel) ): direct_output_label = Gtk.Label.new_with_mnemonic(_("_Direct Out(s)")) direct_output_label.set_halign(Gtk.Align.START) self.properties_grid.attach(direct_output_label, 0, 3, 1, 1) self.direct_output = Gtk.CheckButton() direct_output_label.set_mnemonic_widget(self.direct_output) self.direct_output.set_tooltip_text(_("Add direct post-fader output(s) for channel.")) self.properties_grid.attach(self.direct_output, 1, 3, 1, 1) if isinstance(self, NewChannelDialog) or ( self.channel and isinstance(self.channel, InputChannel) ): solo_label = Gtk.Label.new_with_mnemonic(_("S_olo")) solo_label.set_halign(Gtk.Align.START) grid.attach(solo_label, 0, 3, 1, 1) self.entry_solo_cc = Gtk.SpinButton.new_with_range(-1, 127, 1) self.entry_solo_cc.set_tooltip_text(cc_tooltip.format(param=_("Solo"))) solo_label.set_mnemonic_widget(self.entry_solo_cc) grid.attach(self.entry_solo_cc, 1, 3, 1, 1) self.button_sense_midi_solo = Gtk.Button(_("Learn")) self.button_sense_midi_solo.connect("clicked", self.on_sense_midi_solo_clicked) grid.attach(self.button_sense_midi_solo, 2, 3, 1, 1) self.vbox.show_all() def fill_ui(self): self.entry_name.set_text(self.channel.channel_name) if self.channel.channel.is_stereo: self.stereo.set_active(True) else: self.mono.set_active(True) self.mono.set_sensitive(False) self.stereo.set_sensitive(False) self.entry_volume_cc.set_value(self.channel.channel.volume_midi_cc) self.entry_balance_cc.set_value(self.channel.channel.balance_midi_cc) self.entry_mute_cc.set_value(self.channel.channel.mute_midi_cc) if self.channel and isinstance(self.channel, InputChannel): self.direct_output.set_active(self.channel.wants_direct_output) self.entry_solo_cc.set_value(self.channel.channel.solo_midi_cc) def sense_popup_dialog(self, entry): window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL) window.set_destroy_with_parent(True) window.set_transient_for(self) window.set_decorated(False) window.set_modal(True) window.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) window.set_border_width(10) vbox = Gtk.Box(10, orientation=Gtk.Orientation.VERTICAL) window.add(vbox) window.timeout = 5 label = Gtk.Label( label=_("Please move the MIDI control you want to use for this function.") ) vbox.pack_start(label, True, True, 0) timeout_label = Gtk.Label(label=_("This window will close in 5 seconds.")) vbox.pack_start(timeout_label, True, True, 0) def close_sense_timeout(window, entry): window.timeout -= 1 timeout_label.set_text( _("This window will close in {seconds} seconds.").format(seconds=window.timeout) ) if window.timeout == 0: window.destroy() entry.set_value(self.mixer.last_midi_cc) return False return True window.show_all() GObject.timeout_add_seconds(1, close_sense_timeout, window, entry) def on_sense_midi_volume_clicked(self, *args): self.mixer.last_midi_cc = int(self.entry_volume_cc.get_value()) self.sense_popup_dialog(self.entry_volume_cc) def on_sense_midi_balance_clicked(self, *args): self.mixer.last_midi_cc = int(self.entry_balance_cc.get_value()) self.sense_popup_dialog(self.entry_balance_cc) def on_sense_midi_mute_clicked(self, *args): self.mixer.last_midi_cc = int(self.entry_mute_cc.get_value()) self.sense_popup_dialog(self.entry_mute_cc) def on_sense_midi_solo_clicked(self, *args): self.mixer.last_midi_cc = int(self.entry_solo_cc.get_value()) self.sense_popup_dialog(self.entry_solo_cc) def on_response_cb(self, dlg, response_id, *args): if response_id == Gtk.ResponseType.APPLY: name = self.entry_name.get_text() if name != self.channel.channel_name: self.channel.channel_name = name if self.channel and isinstance(self.channel, InputChannel): if self.direct_output.get_active() and not self.channel.post_fader_output_channel: self.channel.wants_direct_output = True self.app.add_direct_output(self.channel) elif ( not self.direct_output.get_active() and self.channel.post_fader_output_channel ): self.channel.wants_direct_output = False self.channel.post_fader_output_channel.remove() self.channel.post_fader_output_channel = None for control in ("volume", "balance", "mute", "solo"): widget = getattr(self, "entry_{}_cc".format(control), None) if widget is not None: value = int(widget.get_value()) if value != -1: setattr(self.channel.channel, "{}_midi_cc".format(control), value) self.hide() return True def on_entry_name_changed(self, entry): sensitive = False if len(entry.get_text()): if self.channel and self.channel.channel.name == entry.get_text(): sensitive = True elif entry.get_text() not in [x.channel.name for x in self.app.channels] + [ x.channel.name for x in self.app.output_channels ]: sensitive = True self.ok_button.set_sensitive(sensitive) class NewChannelDialog(ChannelPropertiesDialog): def create_ui(self): super().create_ui() self.add_initial_vol_radio() self.vbox.show_all() def add_buttons(self): self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) self.ok_button = self.add_button(Gtk.STOCK_ADD, Gtk.ResponseType.OK) self.ok_button.set_sensitive(False) self.set_default_response(Gtk.ResponseType.OK) def add_initial_vol_radio(self): grid = self.properties_grid grid.attach(Gtk.Label(label=_("Value"), halign=Gtk.Align.START), 0, 2, 1, 1) self.minus_inf = Gtk.RadioButton.new_with_mnemonic(None, _("-_Inf")) self.zero_dB = Gtk.RadioButton.new_with_mnemonic_from_widget(self.minus_inf, _("_0dB")) grid.attach(self.minus_inf, 1, 2, 1, 1) grid.attach(self.zero_dB, 2, 2, 1, 1) class NewInputChannelDialog(NewChannelDialog): def __init__(self, app, title=None): super().__init__(app, title=title or _("New Input Channel")) def fill_ui(self, **values): self.entry_name.set_text(values.get("name", "")) self.direct_output.set_active(values.get("direct_output", True)) # don't set MIDI CCs to previously used values, because they # would overwrite existing mappings, if accepted. self.entry_volume_cc.set_value(-1) self.entry_balance_cc.set_value(-1) self.entry_mute_cc.set_value(-1) self.entry_solo_cc.set_value(-1) self.stereo.set_active(values.get("stereo", True)) self.minus_inf.set_active(values.get("initial_vol", -1) == -1) self.entry_name.grab_focus() def get_result(self): return { "name": self.entry_name.get_text(), "stereo": self.stereo.get_active(), "direct_output": self.direct_output.get_active(), "volume_cc": int(self.entry_volume_cc.get_value()), "balance_cc": int(self.entry_balance_cc.get_value()), "mute_cc": int(self.entry_mute_cc.get_value()), "solo_cc": int(self.entry_solo_cc.get_value()), "initial_vol": 0 if self.zero_dB.get_active() else -1, } class OutputChannelPropertiesDialog(ChannelPropertiesDialog): def create_ui(self): super().create_ui() grid = self.properties_grid color_label = Gtk.Label.new_with_mnemonic(_("_Color")) color_label.set_halign(Gtk.Align.START) grid.attach(color_label, 0, 3, 1, 1) self.color_chooser_button = Gtk.ColorButton() self.color_chooser_button.set_use_alpha(True) color_label.set_mnemonic_widget(self.color_chooser_button) grid.attach(self.color_chooser_button, 1, 3, 2, 1) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.vbox.pack_start(self.create_frame(_("Input Channels"), vbox), True, True, 0) self.display_solo_buttons = Gtk.CheckButton.new_with_mnemonic(_("_Display solo buttons")) vbox.pack_start(self.display_solo_buttons, True, True, 0) self.vbox.show_all() def fill_ui(self): super().fill_ui() self.display_solo_buttons.set_active(self.channel.display_solo_buttons) self.color_chooser_button.set_rgba(self.channel.color) def on_response_cb(self, dlg, response_id, *args): if response_id == Gtk.ResponseType.APPLY: self.channel.display_solo_buttons = self.display_solo_buttons.get_active() self.channel.set_color(self.color_chooser_button.get_rgba()) for inputchannel in self.app.channels: inputchannel.update_control_group(self.channel) return super().on_response_cb(dlg, response_id, *args) class NewOutputChannelDialog(NewChannelDialog, OutputChannelPropertiesDialog): def __init__(self, app, title=None): super().__init__(app, title=title or _("New Output Channel")) def fill_ui(self, **values): self.entry_name.set_text(values.get("name", "")) # TODO: disable mode for output channels as mono output channels may # not be correctly handled yet. self.mono.set_sensitive(False) self.stereo.set_sensitive(False) # don't set MIDI CCs to previously used values, because they # would overwrite existing mappings, if accepted. self.entry_volume_cc.set_value(-1) self.entry_balance_cc.set_value(-1) self.entry_mute_cc.set_value(-1) self.stereo.set_active(values.get("stereo", True)) self.minus_inf.set_active(values.get("initial_vol", -1) == -1) # choose a new random color for each new output channel self.color_chooser_button.set_rgba(random_color()) self.display_solo_buttons.set_active(values.get("display_solo_buttons", False)) self.entry_name.grab_focus() def get_result(self): return { "name": self.entry_name.get_text(), "stereo": self.stereo.get_active(), "volume_cc": int(self.entry_volume_cc.get_value()), "balance_cc": int(self.entry_balance_cc.get_value()), "mute_cc": int(self.entry_mute_cc.get_value()), "display_solo_buttons": self.display_solo_buttons.get_active(), "color": self.color_chooser_button.get_rgba(), "initial_vol": 0 if self.zero_dB.get_active() else -1, } class ControlGroup(Gtk.Alignment): def __init__(self, output_channel, input_channel): super().__init__() self.set(0.5, 0.5, 1, 1) self.output_channel = output_channel self.input_channel = input_channel self.app = input_channel.app self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.add(self.vbox) self.buttons_box = Gtk.Box(False, BUTTON_PADDING, orientation=Gtk.Orientation.HORIZONTAL) set_background_color(self.vbox, output_channel.css_name, output_channel.color) self.vbox.pack_start(self.hbox, True, True, BUTTON_PADDING) hbox_context = self.hbox.get_style_context() hbox_context.add_class("control_group") name = output_channel.channel.name self.label = Gtk.Label(name) self.label.get_style_context().add_class("label") self.label.set_max_width_chars(self.input_channel.label_chars_narrow) self.label.set_ellipsize(Pango.EllipsizeMode.MIDDLE) if len(name) > self.input_channel.label_chars_narrow: self.label.set_tooltip_text(name) self.hbox.pack_start(self.label, False, False, BUTTON_PADDING) self.hbox.pack_end(self.buttons_box, False, False, BUTTON_PADDING) self.mute = mute = Gtk.ToggleButton(_("M")) mute.get_style_context().add_class("mute") mute.set_tooltip_text(_("Mute output channel send")) mute.connect("button-press-event", self.on_mute_button_pressed) mute.connect("toggled", self.on_mute_toggled) self.solo = solo = Gtk.ToggleButton("S") solo.get_style_context().add_class("solo") solo.set_tooltip_text(_("Solo output send")) solo.connect("button-press-event", self.on_solo_button_pressed) solo.connect("toggled", self.on_solo_toggled) self.prefader = pre = Gtk.ToggleButton(_("P")) pre.get_style_context().add_class("prefader") pre.set_tooltip_text(_("Pre (on) / Post (off) fader send")) pre.connect("toggled", self.on_prefader_toggled) self.buttons_box.pack_start(pre, True, True, BUTTON_PADDING) self.buttons_box.pack_start(mute, True, True, BUTTON_PADDING) if self.output_channel.display_solo_buttons: self.buttons_box.pack_start(solo, True, True, BUTTON_PADDING) def update(self): if self.output_channel.display_solo_buttons: if self.solo not in self.buttons_box.get_children(): self.buttons_box.pack_start(self.solo, True, True, BUTTON_PADDING) self.solo.show() else: if self.solo in self.buttons_box.get_children(): self.buttons_box.remove(self.solo) name = self.output_channel.channel.name self.label.set_text(name) if len(name) > self.input_channel.label_chars_narrow: self.label.set_tooltip_text(name) set_background_color(self.vbox, self.output_channel.css_name, self.output_channel.color) def on_mute_toggled(self, button): self.output_channel.channel.set_muted(self.input_channel.channel, button.get_active()) self.app.update_monitor(self.output_channel) def on_mute_button_pressed(self, button, event, *args): if event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK: # Ctrlr+left-click => exclusive mute for output for channel in self.app.channels: if channel is not self.input_channel: ctlgroup = channel.get_control_group(self.output_channel) ctlgroup.mute.set_active(False) return button.get_active() def on_solo_toggled(self, button): self.output_channel.channel.set_solo(self.input_channel.channel, button.get_active()) self.app.update_monitor(self.output_channel) def on_solo_button_pressed(self, button, event, *args): if event.button == 1 and event.state & Gdk.ModifierType.CONTROL_MASK: # Ctrlr+left-click => exclusive solo for output for channel in self.app.channels: if channel is not self.input_channel: ctlgroup = channel.get_control_group(self.output_channel) ctlgroup.solo.set_active(False) return button.get_active() def on_prefader_toggled(self, button): self.output_channel.channel.set_in_prefader( self.input_channel.channel, button.get_active() ) def narrow(self): self.hbox.remove(self.label) self.hbox.set_child_packing(self.buttons_box, True, True, BUTTON_PADDING, Gtk.PackType.END) def widen(self): self.hbox.pack_start(self.label, False, False, BUTTON_PADDING) self.hbox.set_child_packing( self.buttons_box, False, False, BUTTON_PADDING, Gtk.PackType.END ) GObject.signal_new( "input-channel-order-changed", InputChannel, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [GObject.TYPE_STRING, GObject.TYPE_STRING], ) GObject.signal_new( "output-channel-order-changed", OutputChannel, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [GObject.TYPE_STRING, GObject.TYPE_STRING], ) jack_mixer-release-17/jack_mixer/gui.py000066400000000000000000000340201413224161500203050ustar00rootroot00000000000000# This file is part of jack_mixer # # Copyright (C) 2006 Nedko Arnaudov # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. import configparser import logging import os import gi # noqa: F401 from gi.repository import GObject try: import xdg from xdg import BaseDirectory except ImportError: xdg = None from .serialization import SerializedObject log = logging.getLogger(__name__) def lookup_scale(scales, scale_id): for scale in scales: if scale_id == scale.scale_id: return scale return None class Factory(GObject.GObject, SerializedObject): def __init__(self, topwindow, meter_scales, slider_scales): self.languages = [ (None, _("Use system setting")), ("de", "Deutsch"), ("en", "English"), ("es", "Español"), ("fr", "Français"), ] self.midi_behavior_modes = ["Jump To Value", "Pick Up"] GObject.GObject.__init__(self) self.topwindow = topwindow self.meter_scales = meter_scales self.slider_scales = slider_scales self.set_default_preferences() if xdg: self.config = configparser.ConfigParser() self.path = os.path.join( BaseDirectory.save_config_path("jack_mixer"), "preferences.ini" ) if os.path.isfile(self.path): self.read_preferences() else: self.write_preferences() else: log.warning( _("Cannot load PyXDG. ") + _("Your preferences will not be preserved across jack_mixer invocations.") ) def set_default_preferences(self): self.confirm_quit = False self.default_meter_scale = self.meter_scales[0] self.default_project_path = None self.default_slider_scale = self.slider_scales[0] self.language = None self.midi_behavior_mode = 0 self.use_custom_widgets = False self.vumeter_color = "#ccb300" self.vumeter_color_scheme = "default" self.auto_reset_peak_meters = False self.auto_reset_peak_meters_time_seconds = 2.0 self.meter_refresh_period_milliseconds = 33 def read_preferences(self): self.config.read(self.path) self.confirm_quit = self.config.getboolean( "Preferences", "confirm_quit", fallback=self.confirm_quit ) scale_id = self.config["Preferences"]["default_meter_scale"] self.default_meter_scale = lookup_scale(self.meter_scales, scale_id) if not self.default_meter_scale: self.default_meter_scale = self.meter_scales[0] scale_id = self.config["Preferences"]["default_slider_scale"] self.default_slider_scale = lookup_scale(self.slider_scales, scale_id) if not self.default_slider_scale: self.default_slider_scale = self.slider_scales[0] self.default_project_path = self.config["Preferences"].get("default_project_path") self.language = self.config["Preferences"].get("language") try: self.midi_behavior_mode = self.config.getint( "Preferences", "midi_behavior_mode", fallback=self.midi_behavior_mode ) except (TypeError, ValueError): # use default value pass self.use_custom_widgets = self.config.getboolean( "Preferences", "use_custom_widgets", fallback=self.use_custom_widgets ) self.vumeter_color = self.config.get( "Preferences", "vumeter_color", fallback=self.vumeter_color ) self.vumeter_color_scheme = self.config.get( "Preferences", "vumeter_color_scheme", fallback=self.vumeter_color_scheme ) self.auto_reset_peak_meters = self.config.getboolean( "Preferences", "auto_reset_peak_meters", fallback=self.auto_reset_peak_meters ) self.auto_reset_peak_meters_time_seconds = self.config.getfloat( "Preferences", "auto_reset_peak_meters_time_seconds", fallback=self.auto_reset_peak_meters_time_seconds ) self.meter_refresh_period_milliseconds = self.config.getint( "Preferences", "meter_refresh_period_milliseconds", fallback=self.meter_refresh_period_milliseconds ) def write_preferences(self): self.config["Preferences"] = {} self.config["Preferences"]["confirm_quit"] = str(self.confirm_quit) self.config["Preferences"]["default_meter_scale"] = self.default_meter_scale.scale_id self.config["Preferences"]["default_project_path"] = self.default_project_path or "" self.config["Preferences"]["default_slider_scale"] = self.default_slider_scale.scale_id self.config["Preferences"]["language"] = self.language or "" self.config["Preferences"]["midi_behavior_mode"] = str(self.midi_behavior_mode) self.config["Preferences"]["use_custom_widgets"] = str(self.use_custom_widgets) self.config["Preferences"]["vumeter_color"] = self.vumeter_color self.config["Preferences"]["vumeter_color_scheme"] = self.vumeter_color_scheme self.config["Preferences"]["auto_reset_peak_meters"] = str(self.auto_reset_peak_meters) self.config["Preferences"]["auto_reset_peak_meters_time_seconds"] = \ str(self.auto_reset_peak_meters_time_seconds) self.config["Preferences"]["meter_refresh_period_milliseconds"] = \ str(self.meter_refresh_period_milliseconds) with open(self.path, "w") as configfile: self.config.write(configfile) configfile.close() def _update_setting(self, name, value): if value != getattr(self, name): setattr(self, name, value) if xdg: self.write_preferences() signal = "{}-changed".format(name.replace("_", "-")) self.emit(signal, value) def set_confirm_quit(self, confirm_quit): self._update_setting("confirm_quit", confirm_quit) def set_default_meter_scale(self, scale): if scale: self._update_setting("default_meter_scale", scale) else: log.warning( _("Ignoring default_meter_scale setting, because '%s' scale is not known."), scale ) def set_default_project_path(self, path): self._update_setting("default_project_path", path) def set_default_slider_scale(self, scale): if scale: self._update_setting("default_slider_scale", scale) else: log.warning( _("Ignoring default_slider_scale setting, because '%s' scale is not known."), scale ) def set_language(self, lang): self._update_setting("language", lang) def set_midi_behavior_mode(self, mode): self._update_setting("midi_behavior_mode", int(mode)) def set_use_custom_widgets(self, use_custom): self._update_setting("use_custom_widgets", use_custom) def set_vumeter_color(self, color): self._update_setting("vumeter_color", color) def set_vumeter_color_scheme(self, color_scheme): self._update_setting("vumeter_color_scheme", color_scheme) def set_auto_reset_peak_meters(self, auto_reset): self._update_setting("auto_reset_peak_meters", auto_reset) def set_auto_reset_peak_meters_time_seconds(self, time): self._update_setting("auto_reset_peak_meters_time_seconds", time) def set_meter_refresh_period_milliseconds(self, period): self._update_setting("meter_refresh_period_milliseconds", period) def get_confirm_quit(self): return self.confirm_quit def get_default_meter_scale(self): return self.default_meter_scale def get_default_project_path(self): if self.default_project_path: return os.path.expanduser(self.default_project_path) elif xdg: return BaseDirectory.save_data_path("jack_mixer") def get_default_slider_scale(self): return self.default_slider_scale def get_language(self): return self.language def get_midi_behavior_mode(self): return self.midi_behavior_mode def get_use_custom_widgets(self): return self.use_custom_widgets def get_vumeter_color(self): return self.vumeter_color def get_vumeter_color_scheme(self): return self.vumeter_color_scheme def get_auto_reset_peak_meters(self): return self.auto_reset_peak_meters def get_auto_reset_peak_meters_time_seconds(self): return self.auto_reset_peak_meters_time_seconds def get_meter_refresh_period_milliseconds(self): return self.meter_refresh_period_milliseconds def emit_midi_behavior_mode(self): self.emit("midi-behavior-mode-changed", self.midi_behavior_mode) @classmethod def serialization_name(cls): return "gui_factory" def serialize(self, object_backend): object_backend.add_property("confirm-quit", str(self.get_confirm_quit())) object_backend.add_property("default_meter_scale", self.get_default_meter_scale().scale_id) # serialize the value, even if it's empty, not the default fallback directories object_backend.add_property("default_project_path", self.default_project_path or "") object_backend.add_property( "default_slider_scale", self.get_default_slider_scale().scale_id ) object_backend.add_property("midi_behavior_mode", str(self.get_midi_behavior_mode())) object_backend.add_property("use_custom_widgets", str(self.get_use_custom_widgets())) object_backend.add_property("vumeter_color", self.get_vumeter_color()) object_backend.add_property("vumeter_color_scheme", self.get_vumeter_color_scheme()) object_backend.add_property( "auto_reset_peak_meters", str(self.get_auto_reset_peak_meters()) ) object_backend.add_property( "auto_reset_peak_meters_time_seconds", str( self.get_auto_reset_peak_meters_time_seconds() ) ) object_backend.add_property( "meter_refresh_period_milliseconds", str( self.get_meter_refresh_period_milliseconds() ) ) def unserialize_property(self, name, value): if name == "confirm_quit": self.set_confirm_quit(value == "True") return True elif name == "default_meter_scale": self.set_default_meter_scale(lookup_scale(self.meter_scales, value)) return True elif name == "default_project_path": self.set_default_project_path(value or None) return True elif name == "default_slider_scale": self.set_default_slider_scale(lookup_scale(self.slider_scales, value)) return True elif name == "midi_behavior_mode": self.set_midi_behavior_mode(int(value)) return True elif name == "use_custom_widgets": self.set_use_custom_widgets(value == "True") return True elif name == "vumeter_color": self.set_vumeter_color(value) return True elif name == "vumeter_color_scheme": self.set_vumeter_color_scheme(value) return True elif name == "auto_reset_peak_meters": self.set_auto_reset_peak_meters(value) return True elif name == "auto_reset_peak_meters_time_seconds": self.set_auto_reset_peak_meters_time_seconds(float(value)) return True elif name == "meter_refresh_period_milliseconds": self.set_meter_refresh_period_milliseconds(int(value)) return False GObject.signal_new( "confirm-quit-changed", Factory, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [bool], ) GObject.signal_new( "default-meter-scale-changed", Factory, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [GObject.TYPE_PYOBJECT], ) GObject.signal_new( "default-project-path-changed", Factory, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [str], ) GObject.signal_new( "default-slider-scale-changed", Factory, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [GObject.TYPE_PYOBJECT], ) GObject.signal_new( "language-changed", Factory, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [str], ) GObject.signal_new( "midi-behavior-mode-changed", Factory, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [int], ) GObject.signal_new( "use-custom-widgets-changed", Factory, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [bool], ) GObject.signal_new( "vumeter-color-changed", Factory, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [str], ) GObject.signal_new( "vumeter-color-scheme-changed", Factory, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [str], ) GObject.signal_new( "auto-reset-peak-meters-changed", Factory, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [bool], ) GObject.signal_new( "auto-reset-peak-meters-time-seconds-changed", Factory, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [float], ) GObject.signal_new( "meter-refresh-period-milliseconds-changed", Factory, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [int], ) jack_mixer-release-17/jack_mixer/meson.build000066400000000000000000000027161413224161500213200ustar00rootroot00000000000000# https://mesonbuild.com/Python-module.html defines = ['-DLOCALEDIR="@0@"'.format(join_paths(get_option('prefix'), get_option('localedir')))] if get_option('jack-midi').enabled() defines += ['-DHAVE_JACK_MIDI'] endif if get_option('verbose') defines += ['-DLOG_LEVEL=0'] else defines += ['-DLOG_LEVEL=2'] endif # Build and install the extension module module = python.extension_module( '_jack_mixer', [jack_mixer_cython, jack_mixer_sources], dependencies: [ python.dependency(), glib_dep, jack_dep, math_dep, ], include_directories: jack_mixer_inc, c_args: defines, install: true, subdir: 'jack_mixer', ) version_py = configure_file( input: 'version.py.in', output: 'version.py', configuration: { 'VERSION': meson.project_version() } ) # Pure Python sources python_sources = files([ 'abspeak.py', 'app.py', 'channel.py', 'gui.py', '__main__.py', 'meter.py', 'nsmclient.py', 'preferences.py', 'scale.py', 'serialization.py', 'serialization_xml.py', 'slider.py', 'styling.py', ]) # Install pure Python modules python.install_sources( python_sources, version_py, pure: true, subdir: 'jack_mixer', ) # Install application starter script if not get_option('wheel') install_data( '__main__.py', rename: 'jack_mixer', install_dir: bindir, install_mode: 'rwxr-xr-x' ) endif jack_mixer-release-17/jack_mixer/meter.py000066400000000000000000000230171413224161500206410ustar00rootroot00000000000000# This file is part of jack_mixer # # Copyright (C) 2006 Nedko Arnaudov # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. import logging import cairo from gi.repository import Gtk from gi.repository import Gdk from . import scale as scalemod log = logging.getLogger(__name__) METER_MIN_WIDTH = 24 METER_MAX_WIDTH = 40 class MeterWidget(Gtk.DrawingArea): def __init__(self, scale): log.debug("Creating MeterWidget for scale %s", scale) super().__init__() self.color_bg = Gdk.Color(0, 0, 0) self.color_value = None self.color_mark = Gdk.Color(int(65535 * 0.2), int(65535 * 0.2), int(65535 * 0.2)) self.width = 0 self.height = 0 self.cache_surface = None self.min_width = METER_MIN_WIDTH self.preferred_width = METER_MAX_WIDTH self.current_width_size_request = self.preferred_width self.preferred_height = 200 self.set_scale(scale) self.widen() self.connect("draw", self.draw) self.connect("size-allocate", self.on_size_allocate) def narrow(self): return self.widen(False) def widen(self, flag=True): self.current_width_size_request = self.preferred_width if flag else self.min_width self.set_size_request(self.current_width_size_request, self.preferred_height) def set_color(self, color): self.color_value = color self.cache_surface = None self.invalidate_all() def on_expose(self, widget, event): cairo_ctx = widget.window.cairo_create() # set a clip region for the expose event cairo_ctx.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) cairo_ctx.clip() self.draw(cairo_ctx) return False def on_size_allocate(self, widget, allocation): self.width = float(allocation.width) self.height = float(allocation.height) self.font_size = 10 self.cache_surface = None def invalidate_all(self): self.queue_draw_area(0, 0, int(self.width), int(self.height)) def draw_background(self, cairo_ctx): if not self.cache_surface: self.cache_surface = cairo.Surface.create_similar( cairo_ctx.get_target(), cairo.CONTENT_COLOR, int(self.width), int(self.height) ) cache_cairo_ctx = cairo.Context(self.cache_surface) cache_cairo_ctx.set_source_rgba(0, 0, 0, 0) cache_cairo_ctx.rectangle(0, 0, self.width, self.height) cache_cairo_ctx.fill() cache_cairo_ctx.set_source_rgba(0.2, 0.2, 0.2, 1) cache_cairo_ctx.select_font_face("Fixed") cache_cairo_ctx.set_font_size(self.font_size) glyph_width = self.font_size * 3 / 5 # avarage glyph ratio for mark in self.scale.get_marks(): mark_position = int(self.height * (1 - mark.scale)) cache_cairo_ctx.move_to(0, mark_position) cache_cairo_ctx.line_to(self.width, mark_position) cache_cairo_ctx.stroke() x_correction = self.width / 2 - glyph_width * len(mark.text) / 2 cache_cairo_ctx.move_to(x_correction, mark_position - 2) cache_cairo_ctx.show_text(mark.text) cairo_ctx.set_source_surface(self.cache_surface, 0, 0) cairo_ctx.paint() def draw_value(self, cairo_ctx, value, x, width): if self.color_value is not None: cairo_ctx.set_source_rgb( self.color_value.red / 65535.0, self.color_value.green / 65535.0, self.color_value.blue / 65535.0, ) else: height = self.height gradient = cairo.LinearGradient(1, 1, width - 1, height - 1) if self.scale.scale_id == "K20": gradient.add_color_stop_rgb(0, 1, 0, 0) gradient.add_color_stop_rgb(0.38, 1, 1, 0) gradient.add_color_stop_rgb(0.5, 0, 1, 0) gradient.add_color_stop_rgb(1, 0, 0, 1) elif self.scale.scale_id == "K14": gradient.add_color_stop_rgb(0, 1, 0, 0) gradient.add_color_stop_rgb(1 - self.scale.db_to_scale(-14), 1, 1, 0) gradient.add_color_stop_rgb(1 - self.scale.db_to_scale(-24), 0, 1, 0) gradient.add_color_stop_rgb(1, 0, 0, 1) else: gradient.add_color_stop_rgb(0, 1, 0, 0) gradient.add_color_stop_rgb(0.2, 1, 1, 0) gradient.add_color_stop_rgb(1, 0, 1, 0) cairo_ctx.set_source(gradient) cairo_ctx.rectangle(x, self.height * (1 - value), width, self.height * value) cairo_ctx.fill() def draw_peak(self, cairo_ctx, value, x, width): cairo_ctx.set_source_rgb(1, 1, 1) cairo_ctx.rectangle(x, self.height * (1 - value), width, 2.5) cairo_ctx.fill() def set_scale(self, scale): self.kmetering = isinstance(scale, (scalemod.K14, scalemod.K20)) self.scale = scale self.cache_surface = None self.invalidate_all() class MonoMeterWidget(MeterWidget): def __init__(self, scale): super().__init__(scale) self.value = 0.0 self.pk = 0.0 self.raw_value = 0.0 self.raw_pk = 0.0 def draw(self, widget, cairo_ctx): self.draw_background(cairo_ctx) width = self.current_width_size_request / 1.5 x = 0.5 * (self.width - width) self.draw_value(cairo_ctx, self.value, x, width) if self.kmetering: self.draw_peak(cairo_ctx, self.pk, x, width) def set_value(self, value): if value != self.raw_value: self.raw_value = value self.value = self.scale.db_to_scale(value) self.invalidate_all() if value == self.raw_value: return self.raw_value = value old_value = self.value self.value = self.scale.db_to_scale(value) if (abs(old_value-self.value) * self.height) > 1: self.invalidate_all() def set_value_kmeter(self, value, pk): if value == self.raw_value and pk == self.raw_pk: return self.raw_value = value self.raw_pk = pk old_value = self.value old_pk = self.pk self.value = self.scale.db_to_scale(value) self.pk = self.scale.db_to_scale(pk) if (abs(old_value - self.value) * self.height) > 0.01 or ( abs(old_pk - self.pk) * self.height ) > 0.01: self.invalidate_all() class StereoMeterWidget(MeterWidget): def __init__(self, scale): super().__init__(scale) self.pk_left = 0.0 self.pk_right = 0.0 self.left = 0.0 self.right = 0.0 self.raw_left_pk = 0.0 self.raw_right_pk = 0.0 self.raw_left = 0.0 self.raw_right = 0.0 def draw(self, widget, cairo_ctx): self.draw_background(cairo_ctx) width = self.current_width_size_request / 5.0 left_x = 0.5 * self.width - self.current_width_size_request / 5.0 - 0.5 * width right_x = 0.5 * self.width + self.current_width_size_request / 5.0 - 0.5 * width self.draw_value(cairo_ctx, self.left, left_x, width) self.draw_value(cairo_ctx, self.right, right_x, width) if self.kmetering: self.draw_peak(cairo_ctx, self.pk_left, left_x, width) self.draw_peak(cairo_ctx, self.pk_right, right_x, width) def set_values(self, left, right): if left == self.raw_left and right == self.raw_right: return self.raw_left = left self.raw_right = right old_left = self.left old_right = self.right self.left = self.scale.db_to_scale(left) self.right = self.scale.db_to_scale(right) if ( (abs(old_left - self.left) * self.height) > 0.01 or (abs(old_right - self.right) * self.height) > 0.01 ): self.invalidate_all() def set_values_kmeter(self, left, right, pk_l, pk_r): if ( left == self.raw_left and right == self.raw_right and pk_l == self.raw_left_pk and pk_r == self.raw_right_pk ): return self.raw_left = left self.raw_right = right self.raw_left_pk = pk_l self.raw_right_pk = pk_r old_left = self.left old_right = self.right old_pk_left = self.pk_left old_pk_right = self.pk_right self.left = self.scale.db_to_scale(left) self.right = self.scale.db_to_scale(right) self.pk_left = self.scale.db_to_scale(pk_l) self.pk_right = self.scale.db_to_scale(pk_r) if ( (abs(old_left - self.left) * self.height) > 0.01 or (abs(old_right - self.right) * self.height) > 0.01 or (abs(old_pk_left - self.pk_left) * self.height) > 0.01 or (abs(old_pk_right - self.pk_right) * self.height) > 0.01 ): self.invalidate_all() jack_mixer-release-17/jack_mixer/nsmclient.py000066400000000000000000001022671413224161500215260ustar00rootroot00000000000000#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ PyNSMClient - A New Session Manager Client-Library in one file. The Non-Session-Manager by Jonathan Moore Liles : http://non.tuxfamily.org/nsm/ New Session Manager, by LinuxAudio.org: https://new-session-manager.jackaudio.org/ With help from code fragments from https://github.com/attwad/python-osc ( DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE v2 ) MIT License Copyright 2014-2020 Nils Hilbricht https://www.laborejo.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import logging; logger = None #filled by init with prettyName import struct import socket from os import getenv, getpid, kill import os import os.path import shutil from uuid import uuid4 from sys import argv from signal import signal, SIGTERM, SIGINT, SIGKILL #react to exit signals to close the client gracefully. Or kill if the client fails to do so. from urllib.parse import urlparse class _IncomingMessage(object): """Representation of a parsed datagram representing an OSC message. An OSC message consists of an OSC Address Pattern followed by an OSC Type Tag String followed by zero or more OSC Arguments. """ def __init__(self, dgram): #NSM Broadcasts are bundles, but very simple ones. We only need to care about the single message it contains. #Therefore we can strip the bundle prefix and handle it as normal message. if b"#bundle" in dgram: bundlePrefix, singleMessage = dgram.split(b"/", maxsplit=1) dgram = b"/" + singleMessage # / eaten by split self.isBroadcast = True else: self.isBroadcast = False self.LENGTH = 4 #32 bit self._dgram = dgram self._parameters = [] self.parse_datagram() def get_int(self, dgram, start_index): """Get a 32-bit big-endian two's complement integer from the datagram. Args: dgram: A datagram packet. start_index: An index where the integer starts in the datagram. Returns: A tuple containing the integer and the new end index. Raises: ValueError if the datagram could not be parsed. """ try: if len(dgram[start_index:]) < self.LENGTH: raise ValueError('Datagram is too short') return ( struct.unpack('>i', dgram[start_index:start_index + self.LENGTH])[0], start_index + self.LENGTH) except (struct.error, TypeError) as e: raise ValueError('Could not parse datagram %s' % e) def get_string(self, dgram, start_index): """Get a python string from the datagram, starting at pos start_index. We receive always the full string, but handle only the part from the start_index internally. In the end return the offset so it can be added to the index for the next parameter. Each subsequent call handles less of the same string, starting further to the right. According to the specifications, a string is: "A sequence of non-null ASCII characters followed by a null, followed by 0-3 additional null characters to make the total number of bits a multiple of 32". Args: dgram: A datagram packet. start_index: An index where the string starts in the datagram. Returns: A tuple containing the string and the new end index. Raises: ValueError if the datagram could not be parsed. """ #First test for empty string, which is nothing, followed by a terminating \x00 padded by three additional \x00. if dgram[start_index:].startswith(b"\x00\x00\x00\x00"): return "", start_index + 4 #Otherwise we have a non-empty string that must follow the rules of the docstring. offset = 0 try: while dgram[start_index + offset] != 0: offset += 1 if offset == 0: raise ValueError('OSC string cannot begin with a null byte: %s' % dgram[start_index:]) # Align to a byte word. if (offset) % self.LENGTH == 0: offset += self.LENGTH else: offset += (-offset % self.LENGTH) # Python slices do not raise an IndexError past the last index, # do it ourselves. if offset > len(dgram[start_index:]): raise ValueError('Datagram is too short') data_str = dgram[start_index:start_index + offset] return data_str.replace(b'\x00', b'').decode('utf-8'), start_index + offset except IndexError as ie: raise ValueError('Could not parse datagram %s' % ie) except TypeError as te: raise ValueError('Could not parse datagram %s' % te) def get_float(self, dgram, start_index): """Get a 32-bit big-endian IEEE 754 floating point number from the datagram. Args: dgram: A datagram packet. start_index: An index where the float starts in the datagram. Returns: A tuple containing the float and the new end index. Raises: ValueError if the datagram could not be parsed. """ try: return (struct.unpack('>f', dgram[start_index:start_index + self.LENGTH])[0], start_index + self.LENGTH) except (struct.error, TypeError) as e: raise ValueError('Could not parse datagram %s' % e) def parse_datagram(self): try: self._address_regexp, index = self.get_string(self._dgram, 0) if not self._dgram[index:]: # No params is legit, just return now. return # Get the parameters types. type_tag, index = self.get_string(self._dgram, index) if type_tag.startswith(','): type_tag = type_tag[1:] # Parse each parameter given its type. for param in type_tag: if param == "i": # Integer. val, index = self.get_int(self._dgram, index) elif param == "f": # Float. val, index = self.get_float(self._dgram, index) elif param == "s": # String. val, index = self.get_string(self._dgram, index) else: logger.warning("Unhandled parameter type: {0}".format(param)) continue self._parameters.append(val) except ValueError as pe: #raise ValueError('Found incorrect datagram, ignoring it', pe) # Raising an error is not ignoring it! logger.warning("Found incorrect datagram, ignoring it. {}".format(pe)) @property def oscpath(self): """Returns the OSC address regular expression.""" return self._address_regexp @staticmethod def dgram_is_message(dgram): """Returns whether this datagram starts as an OSC message.""" return dgram.startswith(b'/') @property def size(self): """Returns the length of the datagram for this message.""" return len(self._dgram) @property def dgram(self): """Returns the datagram from which this message was built.""" return self._dgram @property def params(self): """Convenience method for list(self) to get the list of parameters.""" return list(self) def __iter__(self): """Returns an iterator over the parameters of this message.""" return iter(self._parameters) class _OutgoingMessage(object): def __init__(self, oscpath): self.LENGTH = 4 #32 bit self.oscpath = oscpath self._args = [] def write_string(self, val): dgram = val.encode('utf-8') diff = self.LENGTH - (len(dgram) % self.LENGTH) dgram += (b'\x00' * diff) return dgram def write_int(self, val): return struct.pack('>i', val) def write_float(self, val): return struct.pack('>f', val) def add_arg(self, argument): t = {str:"s", int:"i", float:"f"}[type(argument)] self._args.append((t, argument)) def build(self): dgram = b'' #OSC Path dgram += self.write_string(self.oscpath) if not self._args: dgram += self.write_string(',') return dgram # Write the parameters. arg_types = "".join([arg[0] for arg in self._args]) dgram += self.write_string(',' + arg_types) for arg_type, value in self._args: f = {"s":self.write_string, "i":self.write_int, "f":self.write_float}[arg_type] dgram += f(value) return dgram class NSMNotRunningError(Exception): """Error raised when environment variable $NSM_URL was not found.""" class NSMClient(object): """The representation of the host programs as NSM sees it. Technically consists of an udp server and a udp client. Does not run an event loop itself and depends on the host loop. E.g. a Qt timer or just a simple while True: sleep(0.1) in Python.""" def __init__(self, prettyName, supportsSaveStatus, saveCallback, openOrNewCallback, exitProgramCallback, hideGUICallback=None, showGUICallback=None, broadcastCallback=None, sessionIsLoadedCallback=None, loggingLevel = "info"): self.nsmOSCUrl = self.getNsmOSCUrl() #this fails and raises NSMNotRunningError if NSM is not available. Host programs can ignore it or exit their program. self.realClient = True self.cachedSaveStatus = None #save status checks for this. global logger logger = logging.getLogger(prettyName) logger.info("import") if loggingLevel == "info" or loggingLevel == 20: logging.basicConfig(level=logging.INFO) #development logger.info("Starting PyNSM2 Client with logging level INFO. Switch to 'error' for a release!") #the NSM name is not ready yet so we just use the pretty name elif loggingLevel == "error" or loggingLevel == 40: logging.basicConfig(level=logging.ERROR) #production else: logging.warning("Unknown logging level: {}. Choose 'info' or 'error'".format(loggingLevel)) logging.basicConfig(level=logging.INFO) #development #given parameters, self.prettyName = prettyName #keep this consistent! Settle for one name. self.supportsSaveStatus = supportsSaveStatus self.saveCallback = saveCallback self.exitProgramCallback = exitProgramCallback self.openOrNewCallback = openOrNewCallback #The host needs to: Create a jack client with ourClientNameUnderNSM - Open the saved file and all its resources self.broadcastCallback = broadcastCallback self.hideGUICallback = hideGUICallback self.showGUICallback = showGUICallback self.sessionIsLoadedCallback = sessionIsLoadedCallback #Reactions get the raw _IncomingMessage OSC object #A client can add to reactions. self.reactions = { "/nsm/client/save" : self._saveCallback, "/nsm/client/show_optional_gui" : lambda msg: self.showGUICallback(), "/nsm/client/hide_optional_gui" : lambda msg: self.hideGUICallback(), "/nsm/client/session_is_loaded" : self._sessionIsLoadedCallback, #Hello source-code reader. You can add your own reactions here by nsmClient.reactions[oscpath]=func, where func gets the raw _IncomingMessage OSC object as argument. #broadcast is handled directly by the function because it has more parameters } #self.discardReactions = set(["/nsm/client/session_is_loaded"]) self.discardReactions = set() #Networking and Init self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #internet, udp self.sock.bind(('', 0)) #pick a free port on localhost. ip, port = self.sock.getsockname() self.ourOscUrl = f"osc.udp://{ip}:{port}/" self.executableName = self.getExecutableName() #UNIX Signals. Used for quit. signal(SIGTERM, self.sigtermHandler) #NSM sends only SIGTERM. #TODO: really? pynsm version 1 handled sigkill as well. signal(SIGINT, self.sigtermHandler) #The following instance parameters are all set in announceOurselves self.serverFeatures = None self.sessionName = None self.ourPath = None self.ourClientNameUnderNSM = None self.ourClientId = None # the "file extension" of ourClientNameUnderNSM self.isVisible = None #set in announceGuiVisibility self.saveStatus = True # true is clean. false means we need saving. self.announceOurselves() assert self.serverFeatures, self.serverFeatures assert self.sessionName, self.sessionName assert self.ourPath, self.ourPath assert self.ourClientNameUnderNSM, self.ourClientNameUnderNSM self.sock.setblocking(False) #We have waited for tha handshake. Now switch blocking off because we expect sock.recvfrom to be empty in 99.99...% of the time so we shouldn't wait for the answer. #After this point the host must include self.reactToMessage in its event loop #We assume we are save at startup. self.announceSaveStatus(isClean = True) logger.info("NSMClient client init complete. Going into listening mode.") def reactToMessage(self): """This is the main loop message. It is added to the clients event loop.""" try: data, addr = self.sock.recvfrom(4096) #4096 is quite big. We don't expect nsm messages this big. Better safe than sorry. However, messages will crash the program if they are bigger than 4096. except BlockingIOError: #happens while no data is received. Has nothing to do with blocking or not. return None msg = _IncomingMessage(data) if msg.oscpath in self.reactions: self.reactions[msg.oscpath](msg) elif msg.oscpath in self.discardReactions: pass elif msg.oscpath == "/reply" and msg.params == ["/nsm/server/open", "Loaded."]: #NSM sends that all programs of the session were loaded. logger.info ("Got /reply Loaded from NSM Server") elif msg.oscpath == "/reply" and msg.params == ["/nsm/server/save", "Saved."]: #NSM sends that all program-states are saved. Does only happen from the general save instruction, not when saving our client individually logger.info ("Got /reply Saved from NSM Server") elif msg.isBroadcast: if self.broadcastCallback: logger.info (f"Got broadcast with messagePath {msg.oscpath} and listOfArguments {msg.params}") self.broadcastCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM, msg.oscpath, msg.params) else: logger.info (f"No callback for broadcast! Got messagePath {msg.oscpath} and listOfArguments {msg.params}") elif msg.oscpath == "/error": logger.warning("Got /error from NSM Server. Path: {} , Parameter: {}".format(msg.oscpath, msg.params)) else: logger.warning("Reaction not implemented:. Path: {} , Parameter: {}".format(msg.oscpath, msg.params)) def send(self, path:str, listOfParameters:list, host=None, port=None): """Send any osc message. Defaults to nsmd URL. Will not wait for an answer but return None.""" if host and port: url = (host, port) else: url = self.nsmOSCUrl msg = _OutgoingMessage(path) for arg in listOfParameters: msg.add_arg(arg) #type is auto-determined by outgoing message self.sock.sendto(msg.build(), url) def getNsmOSCUrl(self): """Return and save the nsm osc url or raise an error""" nsmOSCUrl = getenv("NSM_URL") if not nsmOSCUrl: raise NSMNotRunningError("New-Session-Manager environment variable $NSM_URL not found.") else: #osc.udp://hostname:portnumber/ o = urlparse(nsmOSCUrl) return o.hostname, o.port def getExecutableName(self): """Finding the actual executable name can be a bit hard in Python. NSM wants the real starting point, even if it was a bash script. """ #TODO: I really don't know how to find out the name of the bash script fullPath = argv[0] assert os.path.dirname(fullPath) in os.environ["PATH"], (fullPath, os.path.dirname(fullPath), os.environ["PATH"]) #NSM requires the executable to be in the path. No excuses. This will never happen since the reference NSM server-GUI already checks for this. executableName = os.path.basename(fullPath) assert not "/" in executableName, executableName #see above. return executableName def announceOurselves(self): """Say hello to NSM and tell it we are ready to receive instructions /nsm/server/announce s:application_name s:capabilities s:executable_name i:api_version_major i:api_version_minor i:pid""" def buildClientFeaturesString(): #:dirty:switch:progress: result = [] if self.supportsSaveStatus: result.append("dirty") if self.hideGUICallback and self.showGUICallback: result.append("optional-gui") if result: return ":".join([""] + result + [""]) else: return "" logger.info("Sending our NSM-announce message") announce = _OutgoingMessage("/nsm/server/announce") announce.add_arg(self.prettyName) #s:application_name announce.add_arg(buildClientFeaturesString()) #s:capabilities announce.add_arg(self.executableName) #s:executable_name announce.add_arg(1) #i:api_version_major announce.add_arg(2) #i:api_version_minor announce.add_arg(int(getpid())) #i:pid hostname, port = self.nsmOSCUrl assert hostname, self.nsmOSCUrl assert port, self.nsmOSCUrl self.sock.sendto(announce.build(), self.nsmOSCUrl) #Wait for /reply (aka 'Howdy, what took you so long?) data, addr = self.sock.recvfrom(1024) msg = _IncomingMessage(data) if msg.oscpath == "/error": originalMessage, errorCode, reason = msg.params logger.error("Code {}: {}".format(errorCode, reason)) quit() elif msg.oscpath == "/reply": nsmAnnouncePath, welcomeMessage, managerName, self.serverFeatures = msg.params assert nsmAnnouncePath == "/nsm/server/announce", nsmAnnouncePath logger.info("Got /reply " + welcomeMessage) #Wait for /nsm/client/open data, addr = self.sock.recvfrom(1024) msg = _IncomingMessage(data) assert msg.oscpath == "/nsm/client/open", msg.oscpath self.ourPath, self.sessionName, self.ourClientNameUnderNSM = msg.params self.ourClientId = os.path.splitext(self.ourClientNameUnderNSM)[1][1:] logger.info("Got '/nsm/client/open' from NSM. Telling our client to load or create a file with name {}".format(self.ourPath)) self.openOrNewCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM) #Host function to either load an existing session or create a new one. logger.info("Our client should be done loading or creating the file {}".format(self.ourPath)) replyToOpen = _OutgoingMessage("/reply") replyToOpen.add_arg("/nsm/client/open") replyToOpen.add_arg("{} is opened or created".format(self.prettyName)) self.sock.sendto(replyToOpen.build(), self.nsmOSCUrl) else: raise ValueError("Unexpected message path after announce: {}".format((msg.oscpath, msg.params))) def announceGuiVisibility(self, isVisible): message = "/nsm/client/gui_is_shown" if isVisible else "/nsm/client/gui_is_hidden" self.isVisible = isVisible guiVisibility = _OutgoingMessage(message) logger.info("Telling NSM that our clients switched GUI visibility to: {}".format(message)) self.sock.sendto(guiVisibility.build(), self.nsmOSCUrl) def announceSaveStatus(self, isClean): """Only send to the NSM Server if there was really a change""" if not self.supportsSaveStatus: return if not isClean == self.cachedSaveStatus: message = "/nsm/client/is_clean" if isClean else "/nsm/client/is_dirty" self.cachedSaveStatus = isClean saveStatus = _OutgoingMessage(message) logger.info("Telling NSM that our clients save state is now: {}".format(message)) self.sock.sendto(saveStatus.build(), self.nsmOSCUrl) def _saveCallback(self, msg): logger.info("Telling our client to save as {}".format(self.ourPath)) self.saveCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM) replyToSave = _OutgoingMessage("/reply") replyToSave.add_arg("/nsm/client/save") replyToSave.add_arg("{} saved".format(self.prettyName)) self.sock.sendto(replyToSave.build(), self.nsmOSCUrl) #it is assumed that after saving the state is clear self.announceSaveStatus(isClean = True) def _sessionIsLoadedCallback(self, msg): if self.sessionIsLoadedCallback: logger.info("Received 'Session is Loaded'. Our client supports it. Forwarding message...") self.sessionIsLoadedCallback() else: logger.info("Received 'Session is Loaded'. Our client does not support it, which is the default. Discarding message...") def sigtermHandler(self, signal, frame): """Wait for the user to quit the program The user function does not need to exit itself. Just shutdown audio engines etc. It is possible, that the client does not implement quit properly. In that case NSM protocol demands that we quit anyway. No excuses. Achtung GDB! If you run your program with gdb --args python foo.py the Python signal handler will not work. This has nothing to do with this library. """ logger.info("Telling our client to quit.") self.exitProgramCallback(self.ourPath, self.sessionName, self.ourClientNameUnderNSM) #There is a chance that exitProgramCallback will hang and the program won't quit. However, this is broken design and bad programming. We COULD place a timeout here and just kill after 10s or so, but that would make quitting our responsibility and fixing a broken thing. #If we reach this point we have reached the point of no return. Say goodbye. logger.warning("Client did not quit on its own. Sending SIGKILL.") kill(getpid(), SIGKILL) logger.error("SIGKILL did nothing. Do it manually.") def debugResetDataAndExit(self): """This is solely meant for debugging and testing. The user way of action should be to remove the client from the session and add a new instance, which will get a different NSM-ID. Afterwards we perform a clean exit.""" logger.warning("debugResetDataAndExit will now delete {} and then request an exit.".format(self.ourPath)) if os.path.exists(self.ourPath): if os.path.isfile(self.ourPath): try: os.remove(self.ourPath) except Exception as e: logger.info(e) elif os.path.isdir(self.ourPath): try: shutil.rmtree(self.ourPath) except Exception as e: logger.info(e) else: logger.info("{} does not exist.".format(self.ourPath)) self.serverSendExitToSelf() def serverSendExitToSelf(self): """If you want a very strict client you can block any non-NSM quit-attempts, like ignoring a qt closeEvent, and instead send the NSM Server a request to close this client. This method is a shortcut to do just that. """ logger.info("Sending SIGTERM to ourselves to trigger the exit callback.") #if "server-control" in self.serverFeatures: # message = _OutgoingMessage("/nsm/server/stop") # message.add_arg("{}".format(self.ourClientId)) # self.sock.sendto(message.build(), self.nsmOSCUrl) #else: kill(getpid(), SIGTERM) #this calls the exit callback def serverSendSaveToSelf(self): """Some clients want to offer a manual Save function, mostly for psychological reasons. We offer a clean solution in calling this function which will trigger a round trip over the NSM server so our client thinks it received a Save instruction. This leads to a clean state with a good saveStatus and no required extra functionality in the client.""" logger.info("instructing the NSM-Server to send Save to ourselves.") if "server-control" in self.serverFeatures: #message = _OutgoingMessage("/nsm/server/save") # "Save All" Command. message = _OutgoingMessage("/nsm/gui/client/save") message.add_arg("{}".format(self.ourClientId)) self.sock.sendto(message.build(), self.nsmOSCUrl) else: logger.warning("...but the NSM-Server does not support server control. Server only supports: {}".format(self.serverFeatures)) def changeLabel(self, label:str): """This function is implemented because it is provided by NSM. However, it does not much. The message gets received but is not saved. The official NSM GUI uses it but then does not save it. We would have to send it every startup ourselves. This is fine for us as clients, but you need to provide a GUI field to enter that label.""" logger.info("Telling the NSM-Server that our label is now " + label) message = _OutgoingMessage("/nsm/client/label") message.add_arg(label) #s:label self.sock.sendto(message.build(), self.nsmOSCUrl) def broadcast(self, path:str, arguments:list): """/nsm/server/broadcast s:path [arguments...] We, as sender, will not receive the broadcast back. Broadcasts starting with /nsm are not allowed and will get discarded by the server """ if path.startswith("/nsm"): logger.warning("Attempted broadbast starting with /nsm. Not allwoed") else: logger.info("Sending broadcast " + path + repr(arguments)) message = _OutgoingMessage("/nsm/server/broadcast") message.add_arg(path) for arg in arguments: message.add_arg(arg) #type autodetect self.sock.sendto(message.build(), self.nsmOSCUrl) def importResource(self, filePath): """aka. import into session ATTENTION! You will still receive an absolute path from this function. You need to make sure yourself that this path will not be saved in your save file, but rather use a place- holder that gets replaced by the actual session path each time. A good point is after serialisation. search&replace for the session prefix ("ourPath") and replace it with a tag e.g. . The opposite during load. Only such a behaviour will make your session portable. Do not use the following pattern: An alternative that comes to mind is to only work with relative paths and force your programs workdir to the session directory. Better work with absolute paths internally . Symlinks given path into session dir and returns the linked path relative to the ourPath. It can handles single files as well as whole directories. if filePath is already a symlink we do not follow it. os.path.realpath or os.readlink will not be used. Multilayer links may indicate a users ordering system that depends on abstractions. e.g. with mounted drives under different names which get symlinked to a reliable path. Basically do not question the type of our input filePath. tar with the follow symlink option has os.path.realpath behaviour and therefore is able to follow multiple levels of links anyway. A hardlink does not count as a link and will be detected and treated as real file. Cleaning up a session directory is either responsibility of the user or of our client program. We do not provide any means to unlink or delete files from the session directory. """ #Even if the project was not saved yet now it is time to make our directory in the NSM dir. if not os.path.exists(self.ourPath): os.makedirs(self.ourPath) filePath = os.path.abspath(filePath) #includes normalisation if not os.path.exists(self.ourPath):raise FileNotFoundError(self.ourPath) if not os.path.isdir(self.ourPath): raise NotADirectoryError(self.ourPath) if not os.access(self.ourPath, os.W_OK): raise PermissionError("not writable", self.ourPath) if not os.path.exists(filePath):raise FileNotFoundError(filePath) if os.path.isdir(filePath): raise IsADirectoryError(filePath) if not os.access(filePath, os.R_OK): raise PermissionError("not readable", filePath) filePathInOurSession = os.path.commonprefix([filePath, self.ourPath]) == self.ourPath linkedPath = os.path.join(self.ourPath, os.path.basename(filePath)) linkedPathAlreadyExists = os.path.exists(linkedPath) if not os.access(os.path.dirname(linkedPath), os.W_OK): raise PermissionError("not writable", os.path.dirname(linkedPath)) if filePathInOurSession: #loadResource from our session dir. Portable session, manually copied beforehand or just loading a link again. linkedPath = filePath #we could return here, but we continue to get the tests below. logger.info(f"tried to import external resource {filePath} but this is already in our session directory. We use this file directly instead. ") elif linkedPathAlreadyExists and os.readlink(linkedPath) == filePath: #the imported file already exists as link in our session dir. We do not link it again but simply report the existing link. #We only check for the first target of the existing link and do not follow it through to a real file. #This way all user abstractions and file structures will be honored. linkedPath = linkedPath logger.info(f"tried to import external resource {filePath} but this was already linked to our session directory before. We use the old link: {linkedPath} ") elif linkedPathAlreadyExists: #A new file shall be imported but it would create a linked name which already exists in our session dir. #Because we already checked for a new link to the same file above this means actually linking a different file so we need to differentiate with a unique name firstpart, extension = os.path.splitext(linkedPath) uniqueLinkedPath = firstpart + "." + uuid4().hex + extension assert not os.path.exists(uniqueLinkedPath) os.symlink(filePath, uniqueLinkedPath) logger.info(self.ourClientNameUnderNSM + f":pysm2: tried to import external resource {filePath} but potential target link {linkedPath} already exists. Linked to {uniqueLinkedPath} instead.") linkedPath = uniqueLinkedPath else: #this is the "normal" case. External resources will be linked. assert not os.path.exists(linkedPath) os.symlink(filePath, linkedPath) logger.info(f"imported external resource {filePath} as link {linkedPath}") assert os.path.exists(linkedPath), linkedPath return linkedPath class NullClient(object): """Use this as a drop-in replacement if your program has a mode without NSM but you don't want to change the code itself. This was originally written for programs that have a core-engine and normal mode of operations is a GUI with NSM but they also support commandline-scripts and batch processing. For these you don't want NSM.""" def __init__(self, *args, **kwargs): self.realClient = False self.ourClientNameUnderNSM = "NSM Null Client" def announceSaveStatus(self, *args): pass def announceGuiVisibility(self, *args): pass def reactToMessage(self): pass def importResource(self): return "" def serverSendExitToSelf(self): quit() jack_mixer-release-17/jack_mixer/preferences.py000066400000000000000000000375251413224161500220370ustar00rootroot00000000000000# This file is part of jack_mixer # # Copyright (C) 2006 Nedko Arnaudov # Copyright (C) 2009 Frederic Peters # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. from os.path import expanduser, isdir from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GObject class PreferencesDialog(Gtk.Dialog): def __init__(self, parent): self.app = parent GObject.GObject.__init__(self) self.set_title(_("Preferences")) self.create_ui() self.connect("response", self.on_response_cb) self.connect("delete-event", self.on_response_cb) def create_frame(self, label, child): frame = Gtk.Frame() frame.set_label("") frame.set_border_width(3) frame.set_shadow_type(Gtk.ShadowType.NONE) frame.get_label_widget().set_markup("%s" % label) child.set_margin_top(10) child.set_margin_bottom(10) frame.add(child) return frame def create_ui(self): vbox = self.get_content_area() path_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) path_vbox.set_tooltip_text( _("Set the path where mixer project files are saved and loaded from by default") ) self.path_entry = Gtk.Entry() self.path_entry.connect("changed", self.on_path_entry_changed) path_vbox.pack_start(self.path_entry, False, False, 3) self.project_path_chooser = Gtk.FileChooserButton( title=_("Default Project Path"), action=Gtk.FileChooserAction.SELECT_FOLDER ) project_path = self.app.gui_factory.default_project_path path_vbox.pack_start(self.project_path_chooser, False, False, 3) if project_path: self.path_entry.set_text(project_path) if isdir(expanduser(project_path)): self.project_path_chooser.set_current_folder(expanduser(project_path)) self.project_path_chooser.connect("file-set", self.on_project_path_selected) vbox.pack_start(self.create_frame(_("Default Project Path"), path_vbox), True, True, 0) interface_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.language_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.language_box.set_tooltip_text(_("Set the interface language and localisation")) self.language_combo = self.create_language_combo() interface_vbox.pack_start(self.language_box, True, True, 3) self.language_box.pack_start(Gtk.Label(_("Language:")), False, True, 5) self.language_box.pack_start(self.language_combo, True, True, 0) self.confirm_quit_checkbutton = Gtk.CheckButton(_("Confirm quit")) self.confirm_quit_checkbutton.set_tooltip_text( _("Always ask for confirmation before quitting the application") ) self.confirm_quit_checkbutton.set_active(self.app.gui_factory.get_confirm_quit()) self.confirm_quit_checkbutton.connect("toggled", self.on_confirm_quit_toggled) interface_vbox.pack_start(self.confirm_quit_checkbutton, True, True, 3) self.custom_widgets_checkbutton = Gtk.CheckButton(_("Use custom widgets")) self.custom_widgets_checkbutton.set_tooltip_text( _("Use widgets with custom design for the channel sliders") ) self.custom_widgets_checkbutton.set_active(self.app.gui_factory.get_use_custom_widgets()) self.custom_widgets_checkbutton.connect("toggled", self.on_custom_widget_toggled) interface_vbox.pack_start(self.custom_widgets_checkbutton, True, True, 3) color_tooltip = _("Draw the volume meters with the selected solid color") self.vumeter_color_checkbutton = Gtk.CheckButton(_("Use custom vumeter color")) self.vumeter_color_checkbutton.set_tooltip_text(color_tooltip) self.vumeter_color_checkbutton.set_active( self.app.gui_factory.get_vumeter_color_scheme() == "solid" ) self.vumeter_color_checkbutton.connect("toggled", self.on_vumeter_color_change) interface_vbox.pack_start(self.vumeter_color_checkbutton, True, True, 3) self.custom_color_box = hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.custom_color_box.set_tooltip_text(color_tooltip) interface_vbox.pack_start(hbox, True, True, 3) hbox.set_sensitive(self.vumeter_color_checkbutton.get_active()) hbox.pack_start(Gtk.Label(_("Custom color:")), False, True, 5) self.vumeter_color_picker = Gtk.ColorButton() self.vumeter_color_picker.set_color( Gdk.color_parse(self.app.gui_factory.get_vumeter_color()) ) self.vumeter_color_picker.connect("color-set", self.on_vumeter_color_change) hbox.pack_start(self.vumeter_color_picker, True, True, 0) reset_peak_meter_tooltip = _("Reset the peak meters after the specified time") self.auto_reset_peak_meters_checkbutton = Gtk.CheckButton(_("Auto reset peak meter")) self.auto_reset_peak_meters_checkbutton.set_tooltip_text(reset_peak_meter_tooltip) self.auto_reset_peak_meters_checkbutton.set_active( self.app.gui_factory.get_auto_reset_peak_meters() ) self.auto_reset_peak_meters_checkbutton.connect( "toggled", self.on_auto_reset_peak_meters_toggled ) interface_vbox.pack_start(self.auto_reset_peak_meters_checkbutton, True, True, 3) self.auto_reset_peak_meters_time_seconds_box = hbox = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL ) self.auto_reset_peak_meters_time_seconds_box.set_tooltip_text(reset_peak_meter_tooltip) interface_vbox.pack_start(hbox, True, True, 3) hbox.set_sensitive(self.auto_reset_peak_meters_checkbutton.get_active()) hbox.pack_start(Gtk.Label(_("Time (s):")), False, True, 5) self.auto_reset_peak_meters_time_seconds_spinbutton = ( spinbutton ) = self.create_auto_reset_peak_meters_time_seconds_spinbutton() hbox.pack_start(spinbutton, True, True, 0) self.meter_refresh_period_milliseconds_box = hbox = Gtk.Box( orientation=Gtk.Orientation.HORIZONTAL ) self.meter_refresh_period_milliseconds_box.set_tooltip_text( _("Update the volume level meters with the specified interval in milliseconds") ) interface_vbox.pack_start(hbox, True, True, 3) hbox.pack_start(Gtk.Label(_("Meter Refresh Period (ms):")), False, True, 5) self.meter_refresh_period_milliseconds_spinbutton = ( spinbutton ) = self.create_meter_refresh_period_milliseconds_spinbutton() hbox.pack_start(spinbutton, True, True, 0) vbox.pack_start(self.create_frame(_("Interface"), interface_vbox), True, True, 0) table = Gtk.Table(2, 2, False) table.set_row_spacings(5) table.set_col_spacings(5) meter_scale_tooltip = _("Set the scale for all volume meters") meter_scale_label = Gtk.Label(label=_("Meter scale:")) meter_scale_label.set_tooltip_text(meter_scale_tooltip) table.attach(meter_scale_label, 0, 1, 0, 1) self.meter_scale_combo = self.create_meter_store_and_combo() self.meter_scale_combo.set_tooltip_text(meter_scale_tooltip) table.attach(self.meter_scale_combo, 1, 2, 0, 1) slider_scale_tooltip = _("Set the scale for all volume sliders") slider_scale_label = Gtk.Label(label=_("Slider scale:")) slider_scale_label.set_tooltip_text(slider_scale_tooltip) table.attach(slider_scale_label, 0, 1, 1, 2) self.slider_scale_combo = self.create_slider_store_and_combo() self.slider_scale_combo.set_tooltip_text(slider_scale_tooltip) table.attach(self.slider_scale_combo, 1, 2, 1, 2) vbox.pack_start(self.create_frame(_("Scales"), table), True, True, 0) table = Gtk.Table(1, 2, False) table.set_row_spacings(5) table.set_col_spacings(5) midi_behavior_tooltip = _( "Set how channel volume and balance are controlled via MIDI:\n\n" "- Jump To Value: channel volume or balance is set immediately to received controller value\n" "- Pick Up: control changes are ignored until a controller value near the current value is received\n" ) midi_behavior_label = Gtk.Label(label=_("Control Behavior:")) midi_behavior_label.set_tooltip_text(midi_behavior_tooltip) table.attach(midi_behavior_label, 0, 1, 0, 1) self.midi_behavior_combo = self.create_midi_behavior_combo() self.midi_behavior_combo.set_tooltip_text(midi_behavior_tooltip) table.attach(self.midi_behavior_combo, 1, 2, 0, 1) vbox.pack_start(self.create_frame(_("MIDI"), table), True, True, 0) self.vbox.show_all() self.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE) def create_language_combo(self): combo = Gtk.ComboBoxText() for code, name in self.app.gui_factory.languages: combo.append(code or "", name) combo.set_active_id(self.app.gui_factory.get_language() or "") combo.connect("changed", self.on_language_combo_changed) return combo def create_meter_store_and_combo(self): store = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_PYOBJECT) for scale in self.app.meter_scales: row = scale.scale_id, scale current_iter = store.append(row) if scale is self.app.gui_factory.get_default_meter_scale(): active_iter = current_iter self.meter_store = store meter_scale_combo = Gtk.ComboBox.new_with_model(store) cell = Gtk.CellRendererText() meter_scale_combo.pack_start(cell, True) meter_scale_combo.add_attribute(cell, "text", 0) meter_scale_combo.set_active_iter(active_iter) meter_scale_combo.connect("changed", self.on_meter_scale_combo_changed) return meter_scale_combo def create_slider_store_and_combo(self): store = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_PYOBJECT) for scale in self.app.slider_scales: row = scale.scale_id, scale current_iter = store.append(row) if scale is self.app.gui_factory.get_default_slider_scale(): active_iter = current_iter self.slider_store = store slider_scale_combo = Gtk.ComboBox.new_with_model(store) cell = Gtk.CellRendererText() slider_scale_combo.pack_start(cell, True) slider_scale_combo.add_attribute(cell, "text", 0) slider_scale_combo.set_active_iter(active_iter) slider_scale_combo.connect("changed", self.on_slider_scale_combo_changed) return slider_scale_combo def create_midi_behavior_combo(self): combo = Gtk.ComboBoxText() for i, mode in enumerate(self.app.gui_factory.midi_behavior_modes): combo.append(str(i), mode) combo.set_active(self.app.gui_factory.get_midi_behavior_mode()) combo.connect("changed", self.on_midi_behavior_combo_changed) return combo def create_auto_reset_peak_meters_time_seconds_spinbutton(self): adjustment = Gtk.Adjustment( value=float(self.app.gui_factory.get_auto_reset_peak_meters_time_seconds()), lower=0.1, upper=10.0, step_increment=0.1, page_increment=0.5, page_size=0.0, ) spinbutton = Gtk.SpinButton(adjustment=adjustment, climb_rate=1.0, digits=1) spinbutton.connect("value-changed", self.on_peak_reset_spinbutton_changed) return spinbutton def create_meter_refresh_period_milliseconds_spinbutton(self): adjustment = Gtk.Adjustment( value=int(self.app.gui_factory.get_meter_refresh_period_milliseconds()), lower=1, upper=1000, step_increment=1, page_increment=10, ) spinbutton = Gtk.SpinButton(adjustment=adjustment) spinbutton.connect("value-changed", self.on_meter_refresh_spinbutton_changed) return spinbutton def on_response_cb(self, dlg, response_id, *args): self.app.preferences_dialog = None self.destroy() def on_path_entry_changed(self, *args): path = self.path_entry.get_text().strip() if path: fullpath = expanduser(path) if isdir(fullpath): self.project_path_chooser.set_current_folder(fullpath) self.app.gui_factory.set_default_project_path(path) def on_project_path_selected(self, path_chooser): path = path_chooser.get_filename() self.path_entry.set_text(path) self.app.gui_factory.set_default_project_path(path) def on_language_combo_changed(self, *args): code = self.language_combo.get_active_id() if code != self.app.gui_factory.get_language(): self.app.gui_factory.set_language(code) dlg = Gtk.MessageDialog( parent=self, modal=True, destroy_with_parent=True, message_type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.OK, text=_("You need to restart the application for this setting to take effect."), ) dlg.run() dlg.destroy() def on_meter_scale_combo_changed(self, *args): active_iter = self.meter_scale_combo.get_active_iter() scale = self.meter_store.get(active_iter, 1)[0] self.app.gui_factory.set_default_meter_scale(scale) def on_slider_scale_combo_changed(self, *args): active_iter = self.slider_scale_combo.get_active_iter() scale = self.slider_store.get(active_iter, 1)[0] self.app.gui_factory.set_default_slider_scale(scale) def on_midi_behavior_combo_changed(self, *args): active = self.midi_behavior_combo.get_active() self.app.gui_factory.set_midi_behavior_mode(active) def on_vumeter_color_change(self, *args): color_scheme = "default" if self.vumeter_color_checkbutton.get_active(): color_scheme = "solid" self.app.gui_factory.set_vumeter_color_scheme(color_scheme) color = self.vumeter_color_picker.get_color().to_string() self.app.gui_factory.set_vumeter_color(color) self.custom_color_box.set_sensitive(self.vumeter_color_checkbutton.get_active()) def on_confirm_quit_toggled(self, *args): self.app.gui_factory.set_confirm_quit(self.confirm_quit_checkbutton.get_active()) def on_custom_widget_toggled(self, *args): self.app.gui_factory.set_use_custom_widgets(self.custom_widgets_checkbutton.get_active()) def on_auto_reset_peak_meters_toggled(self, *args): self.app.gui_factory.set_auto_reset_peak_meters( self.auto_reset_peak_meters_checkbutton.get_active() ) self.auto_reset_peak_meters_time_seconds_box.set_sensitive( self.auto_reset_peak_meters_checkbutton.get_active() ) def on_peak_reset_spinbutton_changed(self, *args): self.app.gui_factory.set_auto_reset_peak_meters_time_seconds( self.auto_reset_peak_meters_time_seconds_spinbutton.get_value() ) def on_meter_refresh_spinbutton_changed(self, *args): self.app.gui_factory.set_meter_refresh_period_milliseconds( self.meter_refresh_period_milliseconds_spinbutton.get_value_as_int() ) jack_mixer-release-17/jack_mixer/scale.py000066400000000000000000000177131413224161500206220ustar00rootroot00000000000000# This file is part of jack_mixer # # Copyright (C) 2006 Nedko Arnaudov # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. import logging import math from ._jack_mixer import Scale log = logging.getLogger(__name__) class Mark: """Encapsulates scale linear function edge and coefficients for scale = a * dB + b formula""" def __init__(self, db, scale, text=None): self.db = db self.scale = scale if text is None: # TODO: l10n self.text = "%.0f" % math.fabs(db) else: self.text = text class Base: """Scale abstraction, various scale implementation derive from this class""" def __init__(self, scale_id, description): self.marks = [] self.scale_id = scale_id self.description = description self.scale = Scale() def add_threshold(self, db, scale, is_mark, text=None): self.scale.add_threshold(db, scale) if is_mark: self.marks.append(Mark(db, scale, text)) def calculate_coefficients(self): self.scale.calculate_coefficients() def db_to_scale(self, db): """Convert dBFS value to number in range 0.0-1.0 used in GUI""" # log.debug("db_to_scale(%f)", db) return self.scale.db_to_scale(db) def scale_to_db(self, scale): """Convert number in range 0.0-1.0 used in GUI to dBFS value""" return self.scale.scale_to_db(scale) def add_mark(self, db): self.marks.append(Mark(db, -1.0)) def get_marks(self): return self.marks def scale_marks(self): for i in self.marks: if i.scale == -1.0: i.scale = self.db_to_scale(i.db) # IEC 60268-18 Peak programme level meters - Digital audio peak level meter # Adapted from meterbridge, may be wrong, I'm not buying standards, even if they cost $45 # If someone has the standard, please either share it with me or fix the code. class IEC268(Base): """IEC 60268-18 Peak programme level meters - Digital audio peak level meter""" def __init__(self): Base.__init__( self, "iec_268", _("IEC 60268-18 Peak programme level meters - Digital audio peak level meter"), ) self.add_threshold(-70.0, 0.0, False) self.add_threshold(-60.0, 0.05, True) self.add_threshold(-50.0, 0.075, True) self.add_threshold(-40.0, 0.15, True) self.add_mark(-35.0) self.add_threshold(-30.0, 0.3, True) self.add_mark(-25.0) self.add_threshold(-20.0, 0.5, True) self.add_mark(-15.0) self.add_mark(-10.0) self.add_mark(-5.0) self.add_threshold(0.0, 1.0, True) self.calculate_coefficients() self.scale_marks() class IEC268Minimalistic(Base): """IEC 60268-18 Peak programme level meters - Digital audio peak level meter, fewer marks""" def __init__(self): Base.__init__( self, "iec_268_minimalistic", _( "IEC 60268-18 Peak programme level meters - " "Digital audio peak level meter, fewer marks" ), ) self.add_threshold(-70.0, 0.0, False) self.add_threshold(-60.0, 0.05, True) self.add_threshold(-50.0, 0.075, False) self.add_threshold(-40.0, 0.15, True) self.add_threshold(-30.0, 0.3, True) self.add_threshold(-20.0, 0.5, True) self.add_mark(-10.0) self.add_threshold(0.0, 1.0, True) self.calculate_coefficients() self.scale_marks() class Linear70dB(Base): """Linear scale with range from -70 to 0 dBFS""" def __init__(self): Base.__init__(self, "linear_70dB", _("Linear scale with range from -70 to 0 dBFS")) self.add_threshold(-70.0, 0.0, False) self.add_mark(-60.0) self.add_mark(-50.0) self.add_mark(-40.0) self.add_mark(-35.0) self.add_mark(-30.0) self.add_mark(-25.0) self.add_mark(-20.0) self.add_mark(-15.0) self.add_mark(-10.0) self.add_mark(-5.0) self.add_threshold(0.0, 1.0, True) self.calculate_coefficients() self.scale_marks() class Linear30dB(Base): """Linear scale with range from -30 to +30 dBFS""" def __init__(self): Base.__init__(self, "linear_30dB", _("Linear scale with range from -30 to +30 dBFS")) self.add_threshold(-30.0, 0.0, False) self.add_threshold(+30.0, 1.0, True) self.calculate_coefficients() self.scale_marks() class K20(Base): """K20 scale""" def __init__(self): Base.__init__(self, "K20", _("K20 scale")) self.add_mark(-200, "") self.add_mark(-60, "-40") self.add_mark(-55, "") self.add_mark(-50, "-30") self.add_mark(-45, "") self.add_mark(-40, "-20") self.add_mark(-35, "") self.add_mark(-30, "-10") self.add_mark(-26, "-6") self.add_mark(-23, "-3") self.add_mark(-20, "0") self.add_mark(-17, "3") self.add_mark(-14, "6") self.add_mark(-10, "10") self.add_mark(-5, "15") self.add_mark(0, "20") def db_to_scale(self, db): v = math.pow(10.0, db / 20.0) return self.mapk20(v) / 450.0 def add_mark(self, db, text): self.marks.append(Mark(db, self.db_to_scale(db), text)) def mapk20(self, v): if v < 0.001: return 24000 * v v = math.log10(v) + 3 if v < 2.0: return 24.3 + v * (100 + v * 16) if v > 3.0: v = 3.0 return v * 161.7 - 35.1 class K14(Base): """K14 scale""" def __init__(self): Base.__init__(self, "K14", _("K14 scale")) self.add_mark(-200, "") self.add_mark(-54, "-40") self.add_mark(-35 - 14, "") self.add_mark(-30 - 14, "-30") self.add_mark(-25 - 14, "") self.add_mark(-20 - 14, "-20") self.add_mark(-15 - 14, "") self.add_mark(-10 - 14, "-10") self.add_mark(-6 - 14, "-6") self.add_mark(-3 - 14, "-3") self.add_mark(-14, "0") self.add_mark(3 - 14, "3") self.add_mark(6 - 14, "6") self.add_mark(10 - 14, "10") self.add_mark(14 - 14, "14") def db_to_scale(self, db): v = math.pow(10.0, db / 20.0) return self.mapk14(v) / 448.3 def add_mark(self, db, text): self.marks.append(Mark(db, self.db_to_scale(db), text)) def mapk14(self, v): if v < 0.002: return int(12000 * v) v = math.log10(v) + 2.7 if v < 2.0: return int(20.3 + v * (80 + v * 32)) if v > 2.7: v = 2.7 return int(v * 200 - 91.7) def scale_test1(scale): for i in range(-97 * 2, 1, 1): db = float(i) / 2.0 print("%5.1f dB maps to %f" % (db, scale.db_to_scale(db))) def scale_test2(scale): for i in range(101): s = float(i) / 100.0 print("%.2f maps to %.1f dB" % (s, scale.scale_to_db(s))) def print_db_to_scale(scale, db): print("%-.1f dB maps to %f" % (db, scale.db_to_scale(db))) def scale_test3(scale): print_db_to_scale(scale, +77.0) print_db_to_scale(scale, +7.0) print_db_to_scale(scale, 0.0) print_db_to_scale(scale, -107.0) def _test(*args, **kwargs): scale = Linear30dB() scale_test1(scale) scale_test2(scale) scale_test3(scale) if __name__ == "__main__": _test() jack_mixer-release-17/jack_mixer/serialization.py000066400000000000000000000075051413224161500224060ustar00rootroot00000000000000# This file is part of jack_mixer # # Copyright (C) 2006 Nedko Arnaudov # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. import logging log = logging.getLogger(__name__) class SerializationBackend: """Base class for serialization backends""" def get_root_serialization_object(self, name): """Returns serialization object where properties of root object will be serialized to""" # this method should never be called for the base class raise NotImplementedError def get_child_serialization_object(self, name, backend_object): # this method should never be called for the base class raise NotImplementedError class SerializationObjectBackend: """Base class for serialization backend objects where real object properties will be serialized to or unserialized from.""" def add_property(self, name, value): """Serialize particular property""" pass def get_childs(self): pass def get_properties(self): pass def serialization_name(self): return None class SerializedObject: """Base class for object supporting serialization""" def serialization_name(self): return None def serialize(self, object_backend): """Serialize properties of called object into supplied serialization_object_backend""" pass def serialization_get_childs(self): """Get child objects tha required and support serialization""" return [] def unserialize_property(self, name, value): pass def unserialize_child(self, name): return None class Serializator: def __init__(self): pass def serialize(self, root, backend): self.serialize_one( backend, root, backend.get_root_serialization_object(root.serialization_name()) ) def unserialize(self, root, backend): backend_object = backend.get_root_unserialization_object(root.serialization_name()) if backend_object is None: return False return self.unserialize_one(backend, root, backend_object) def unserialize_one(self, backend, object, backend_object): log.debug("Unserializing %r.", object) properties = backend_object.get_properties() for name, value in properties.items(): log.debug("Property %s = %s", name, value) if not object.unserialize_property(name, value): return False backend_childs = backend_object.get_childs() for backend_child in backend_childs: name = backend_child.serialization_name() child = object.unserialize_child(name) if not child: return False if not self.unserialize_one(backend, child, backend_child): return False return True def serialize_one(self, backend, object, backend_object): object.serialize(backend_object) childs = object.serialization_get_childs() for child in childs: log.debug("Serializing child %r.", child) self.serialize_one( backend, child, backend.get_child_serialization_object(child.serialization_name(), backend_object), ) jack_mixer-release-17/jack_mixer/serialization_xml.py000066400000000000000000000062121413224161500232600ustar00rootroot00000000000000# This file is part of jack_mixer # # Copyright (C) 2006 Nedko Arnaudov # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. import xml.dom import xml.dom.minidom from .serialization import SerializationBackend class XmlSerializationError(Exception): pass class InvalidDocumentTypeError(XmlSerializationError): pass class XmlSerialization(SerializationBackend): def get_root_serialization_object(self, name): self.doc = xml.dom.getDOMImplementation().createDocument( xml.dom.EMPTY_NAMESPACE, name, None ) return XmlSerializationObject(self.doc, self.doc.documentElement) def get_root_unserialization_object(self, name): if name != self.doc.documentElement.nodeName: return None return XmlSerializationObject(self.doc, self.doc.documentElement) def get_child_serialization_object(self, name, backend_object): child = self.doc.createElement(name) backend_object.element.appendChild(child) return XmlSerializationObject(self.doc, child) def save(self, file): file.write(self.doc.toprettyxml()) def load(self, file, name): self.doc = xml.dom.minidom.parse(file) document_type = self.doc.documentElement.nodeName if document_type != name: raise InvalidDocumentTypeError( _("Document type '{type}' not supported.").format(type=document_type) ) class XmlSerializationObject: def __init__(self, doc, element): self.doc = doc self.element = element def add_property(self, name, value): self.add_property_as_attribute(name, value) def add_property_as_attribute(self, name, value): self.element.setAttribute(name, value) # def add_property_as_child_element(self, name, value): # child = self.doc.createElement(name) # value = self.doc.createTextNode(value) # child.appendChild(value) # self.element.appendChild(child) def get_childs(self): child_elements = self.element.childNodes childs = [] for child in child_elements: if child.nodeType == child.ELEMENT_NODE: childs.append(XmlSerializationObject(self.doc, child)) return childs def get_properties(self): properties = self.element.attributes dictionary = {} for i in range(properties.length): dictionary[properties.item(i).name] = properties.item(i).nodeValue return dictionary def serialization_name(self): return self.element.nodeName jack_mixer-release-17/jack_mixer/slider.py000066400000000000000000000372421413224161500210140ustar00rootroot00000000000000# This file is part of jack_mixer # # Copyright (C) 2006 Nedko Arnaudov # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. import logging import cairo from gi.repository import Gdk, GObject, Gtk log = logging.getLogger(__name__) FADER_MIN_WIDTH = 24 FADER_MAX_WIDTH = 40 class AdjustmentdBFS(Gtk.Adjustment): def __init__(self, scale, default_db, step_inc): self.default_value = scale.db_to_scale(default_db) self.db = default_db self.scale = scale self.step_increment = step_inc super().__init__(self.default_value, 0.0, 1.0, step_inc) self.connect("value-changed", self.on_value_changed) self.disable_value_notify = False def step_up(self): self.set_value(self.get_value() + self.step_increment) def step_down(self): self.set_value(self.get_value() - self.step_increment) def reset(self): self.set_value(self.default_value) def get_value_db(self): return self.db def set_value_db(self, db, from_midi=False): self.db = db self.disable_value_notify = True self.set_value(self.scale.db_to_scale(db)) self.disable_value_notify = False if from_midi: self.emit("volume-changed-from-midi") else: self.emit("volume-changed") def on_value_changed(self, adjustment): if not self.disable_value_notify: self.db = self.scale.scale_to_db(self.get_value()) self.emit("volume-changed") def set_scale(self, scale): self.scale = scale self.disable_value_notify = True self.set_value(self.scale.db_to_scale(self.db)) self.disable_value_notify = False GObject.signal_new( "volume-changed", AdjustmentdBFS, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [], ) GObject.signal_new( "volume-changed-from-midi", AdjustmentdBFS, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [], ) class BalanceAdjustment(Gtk.Adjustment): def __init__(self): super().__init__(0.0, -1.0, 1.0, 0.1) self.connect("value-changed", self.on_value_changed) self.disable_value_notify = False def set_balance(self, value, from_midi=False): self.disable_value_notify = True self.set_value(value) self.disable_value_notify = False if not from_midi: self.emit("balance-changed") def on_value_changed(self, adjustment): if not self.disable_value_notify: self.emit("balance-changed") GObject.signal_new( "balance-changed", BalanceAdjustment, GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION, None, [], ) class VolumeSlider(Gtk.Scale): def __init__(self, adjustment): super().__init__(orientation=Gtk.Orientation.VERTICAL) self.adjustment = adjustment self.set_adjustment(adjustment) self.set_draw_value(False) self.set_inverted(True) self._button_down = False self._button_down_y = 0 self._button_down_value = 0 self.min_width = FADER_MIN_WIDTH self.preferred_width = FADER_MAX_WIDTH self.preferred_height = 200 self.connect("button-press-event", self.button_press_event) self.connect("button-release-event", self.button_release_event) self.connect("motion-notify-event", self.motion_notify_event) self.connect("scroll-event", self.scroll_event) def narrow(self): return self.widen(False) def widen(self, flag=True): self.set_size_request( self.preferred_width if flag else self.min_width, self.preferred_height ) def button_press_event(self, widget, event): if event.button == 1: if event.state & Gdk.ModifierType.CONTROL_MASK: if event.type == Gdk.EventType.BUTTON_PRESS: self.adjustment.set_value_db(0) return True elif event.type == Gdk.EventType.BUTTON_PRESS: self._button_down = True self._button_down_y = event.y self._button_down_value = self.adjustment.get_value() return True elif event.type == Gdk.EventType._2BUTTON_PRESS: self.adjustment.set_value(0) return True return False def button_release_event(self, widget, event): self._button_down = False return False def motion_notify_event(self, widget, event): slider_length = widget.get_allocation().height - widget.get_style_context().get_property( "min-height", Gtk.StateFlags.NORMAL ) if self._button_down: delta_y = (self._button_down_y - event.y) / slider_length y = self._button_down_value + delta_y if y >= 1: y = 1 elif y <= 0: y = 0 self.adjustment.set_value(y) return True def scroll_event(self, widget, event): delta = self.adjustment.step_increment value = self.adjustment.get_value() if event.direction == Gdk.ScrollDirection.UP: y = value + delta elif event.direction == Gdk.ScrollDirection.DOWN: y = value - delta elif event.direction == Gdk.ScrollDirection.SMOOTH: y = value - event.delta_y * delta if y >= 1: y = 1 elif y <= 0: y = 0 self.adjustment.set_value(y) return True class BalanceSlider(Gtk.Scale): def __init__(self, adjustment, preferred_width, preferred_height): super().__init__(orientation=Gtk.Orientation.HORIZONTAL) self.adjustment = adjustment self.set_adjustment(adjustment) self.set_has_origin(False) self.set_draw_value(False) self.set_property("has-tooltip", True) self._preferred_width = preferred_width self._preferred_height = preferred_height self._button_down = False self.add_mark(-1.0, Gtk.PositionType.TOP) self.add_mark(0.0, Gtk.PositionType.TOP) self.add_mark(1.0, Gtk.PositionType.TOP) self.connect("button-press-event", self.on_button_press_event) self.connect("button-release-event", self.on_button_release_event) self.connect("motion-notify-event", self.on_motion_notify_event) self.connect("scroll-event", self.on_scroll_event) self.connect("query-tooltip", self.on_query_tooltip) def get_preferred_width(self): return self._preferred_width def get_preferred_height(self): return self._preferred_height def on_button_press_event(self, widget, event): if event.button == 1: if event.type == Gdk.EventType.BUTTON_PRESS: self._button_down = True self._button_down_x = event.x self._button_down_value = self.get_value() return True elif event.type == Gdk.EventType._2BUTTON_PRESS: self.adjustment.set_balance(0) return True return False def on_button_release_event(self, widget, event): self._button_down = False return False def on_motion_notify_event(self, widget, event): slider_length = widget.get_allocation().width - widget.get_style_context().get_property( "min-width", Gtk.StateFlags.NORMAL ) if self._button_down: delta_x = (event.x - self._button_down_x) / slider_length x = self._button_down_value + 2 * delta_x self.adjustment.set_balance(min(1, max(x, -1))) return True return False def on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip, *args): val = int(self.adjustment.get_value() * 50) if val == 0: tooltip.set_text(_("Center")) else: tooltip.set_text( _("Left: {left} / Right: {right}").format(left=50 - val, right=val + 50) ) return True def on_scroll_event(self, widget, event): delta = self.get_adjustment().get_step_increment() value = self.get_value() if event.direction == Gdk.ScrollDirection.UP: x = value - delta elif event.direction == Gdk.ScrollDirection.DOWN: x = value + delta elif event.direction == Gdk.ScrollDirection.SMOOTH: x = value - event.delta_y * delta self.set_value(min(1, max(x, -1))) return True class CustomSliderWidget(Gtk.DrawingArea): def __init__(self, adjustment): super().__init__() self.adjustment = adjustment self._button_down = False self._button_down_y = 0 self._button_down_value = 0 self.min_width = FADER_MIN_WIDTH self.preferred_width = FADER_MAX_WIDTH self.preferred_height = 200 self.connect("draw", self.on_expose) self.connect("size_allocate", self.on_size_allocate) adjustment.connect("value-changed", self.on_value_changed) self.connect("button-release-event", self.on_button_release_event) self.connect("button-press-event", self.on_button_press_event) self.connect("motion-notify-event", self.on_motion_notify_event) self.connect("scroll-event", self.on_scroll) self.set_events( Gdk.EventMask.BUTTON1_MOTION_MASK | Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK ) def narrow(self): return self.widen(False) def widen(self, flag=True): self.set_size_request( self.preferred_width if flag else self.min_width, self.preferred_height ) def on_button_press_event(self, widget, event): log.debug("Mouse button %u pressed %ux%u", event.button, event.x, event.y) if event.button == 1: if ( event.state & Gdk.ModifierType.CONTROL_MASK and event.type == Gdk.EventType.BUTTON_PRESS ): self.adjustment.set_value_db(0) return True elif event.type == Gdk.EventType.BUTTON_PRESS: self._button_down = True self._button_down_y = event.y self._button_down_value = self.adjustment.get_value() return True elif event.type == Gdk.EventType._2BUTTON_PRESS: self.adjustment.set_value(0) return True return False def on_button_release_event(self, widget, event): self._button_down = False return False def on_motion_notify_event(self, widget, event): slider_length = self.slider_rail_height if self._button_down: delta_y = (self._button_down_y - event.y) / slider_length y = self._button_down_value + delta_y if y >= 1: y = 1 elif y <= 0: y = 0 self.adjustment.set_value(y) return True def on_value_changed(self, adjustment): self.invalidate_all() def on_expose(self, widget, cairo_ctx): self.draw(cairo_ctx) return False def on_scroll(self, widget, event): delta = self.adjustment.step_increment y = self.adjustment.get_value() if event.direction == Gdk.ScrollDirection.UP: y = y + delta elif event.direction == Gdk.ScrollDirection.DOWN: y = y - delta elif event.direction == Gdk.ScrollDirection.SMOOTH: y = y - event.delta_y * delta if y >= 1: y = 1 elif y <= 0: y = 0 self.adjustment.set_value(y) return True def on_size_allocate(self, widget, allocation): self.width = float(allocation.width) self.height = float(allocation.height) self.font_size = 10 def on_size_request(self, widget, requisition): requisition.width = 666 def invalidate_all(self): if hasattr(self, "width") and hasattr(self, "height"): self.queue_draw_area(0, 0, int(self.width), int(self.height)) def draw(self, cairo_ctx): if self.has_focus(): state = Gtk.StateType.PRELIGHT else: state = Gtk.StateType.NORMAL # cairo_ctx.rectangle(0, 0, self.width, self.height) # cairo_ctx.set_source_color(self.style.bg[state]) # cairo_ctx.fill_preserve() # Gdk.cairo_set_source_color(cairo_ctx, # self.get_style_context().get_color(state).to_color()) # cairo_ctx.stroke() slider_knob_width = self.preferred_width * 3 / 4 if self.width * 3 / 4 > \ self.preferred_width * 3 / 4 else self.width * 3 / 4 slider_knob_height = slider_knob_width * 2 slider_knob_height -= slider_knob_height % 2 slider_knob_height += 1 slider_x = self.width / 2 cairo_ctx.set_line_cap(cairo.LineCap.ROUND) cairo_ctx.set_line_width(slider_knob_width / 8.0) # slider rail Gdk.cairo_set_source_color(cairo_ctx, self.get_style_context().get_color(state).to_color()) self.slider_rail_up = slider_knob_height / 2 self.slider_rail_height = self.height - 2 * self.slider_rail_up cairo_ctx.move_to(slider_x, self.slider_rail_up) cairo_ctx.line_to(slider_x, self.slider_rail_height + self.slider_rail_up) cairo_ctx.stroke() # slider knob slider_y = round( self.slider_rail_up + self.slider_rail_height * (1 - self.adjustment.get_value()) ) lg = cairo.LinearGradient( slider_x - float(slider_knob_width) / 2, slider_y - slider_knob_height / 2, slider_x - float(slider_knob_width) / 2, slider_y + slider_knob_height / 2, ) slider_alpha = 1.0 lg.add_color_stop_rgba(0, 0.55, 0.55, 0.55, slider_alpha) lg.add_color_stop_rgba(0.1, 0.65, 0.65, 0.65, slider_alpha) lg.add_color_stop_rgba(0.1, 0.75, 0.75, 0.75, slider_alpha) lg.add_color_stop_rgba(0.125, 0.75, 0.75, 0.75, slider_alpha) lg.add_color_stop_rgba(0.125, 0.15, 0.15, 0.15, slider_alpha) lg.add_color_stop_rgba(0.475, 0.35, 0.35, 0.35, slider_alpha) lg.add_color_stop_rgba(0.475, 0, 0, 0, slider_alpha) lg.add_color_stop_rgba(0.525, 0, 0, 0, slider_alpha) lg.add_color_stop_rgba(0.525, 0.35, 0.35, 0.35, slider_alpha) lg.add_color_stop_rgba(0.875, 0.65, 0.65, 0.65, slider_alpha) lg.add_color_stop_rgba(0.875, 0.75, 0.75, 0.75, slider_alpha) lg.add_color_stop_rgba(0.900, 0.75, 0.75, 0.75, slider_alpha) lg.add_color_stop_rgba(0.900, 0.15, 0.15, 0.15, slider_alpha) lg.add_color_stop_rgba(1.000, 0.10, 0.10, 0.10, slider_alpha) cairo_ctx.rectangle( slider_x - float(slider_knob_width) / 2, slider_y - slider_knob_height / 2, float(slider_knob_width), slider_knob_height, ) Gdk.cairo_set_source_color( cairo_ctx, self.get_style_context().get_background_color(state).to_color() ) cairo_ctx.fill_preserve() cairo_ctx.set_source(lg) cairo_ctx.fill() jack_mixer-release-17/jack_mixer/styling.py000066400000000000000000000122261413224161500212160ustar00rootroot00000000000000# This file is part of jack_mixer # # Copyright (C) 2006-2009 Nedko Arnaudov # Copyright (C) 2009-2020 Frederic Peters et al. # Copyright (C) 2020-2021 Christopher Arndt # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. from random import random import gi # noqa: F401 from gi.repository import Gtk from gi.repository import Gdk # CSS widget styling DEFAULT_CSS = """\ /* Global color definitions */ @define-color monitor_bgcolor_hover #C0BFBC; @define-color monitor_bgcolor_checked #9A9996; @define-color mute_bgcolor_hover #FFBC80; @define-color mute_bgcolor_checked #FF7800; @define-color solo_bgcolor_hover #76A28E; @define-color solo_bgcolor_checked #26A269; @define-color prefader_bgcolor_hover #A6C2E4; @define-color prefader_bgcolor_checked #3584E4; /* Channel strips */ .top_label { padding: 0px .1em; min-height: 1.5rem; } .wide { font-size: medium; } .narrow { font-size: smaller; } .vbox_fader { border: 1px inset #111; } .readout { font-size: 80%; margin: .1em; padding: 0; border: 1px inset #111; color: white; background-color: #333; background-image: none; } /* Channel buttons */ button { padding: 0px .2em; } button.prefader_meter { font-size: smaller; } button.monitor:hover, button.mute:hover, button.solo:hover, button.prefader:hover, button.prefader_meter:hover, button.monitor:checked, button.mute:checked, button.solo:checked, button.prefader:checked, button.prefader_meter:checked { color: white; text-shadow: unset; background-image: none; } button.monitor:hover { background-color: @monitor_bgcolor_hover; } button.monitor:checked { background-color: @monitor_bgcolor_checked; } button.mute:hover { background-color: @mute_bgcolor_hover; } button.mute:checked { background-color: @mute_bgcolor_checked; } button.solo:hover { background-color: @solo_bgcolor_hover; } button.solo:checked { background-color: @solo_bgcolor_checked; } button.prefader:hover { background-color: @prefader_bgcolor_hover; } button.prefader:checked { background-color: @prefader_bgcolor_checked; } button.prefader_meter:hover { background-color: @prefader_bgcolor_hover; } button.prefader_meter:checked { background-color: @prefader_bgcolor_checked; } /* Control groups */ .control_group { min-width: 0px; padding: 0px; } .control_group .label, .control_group .mute, .control_group .prefader, .control_group .solo { font-size: smaller; padding: 0px .1em; } .control_group .mute:hover, .control_group .solo:hover, .control_group .prefader:hover, .control_group .mute:checked, .control_group .solo:checked, .control_group .prefader:checked { color: white; text-shadow: unset; background-image: none; } .control_group .mute:hover { background-color: @mute_bgcolor_hover; } .control_group .mute:checked { background-color:@mute_bgcolor_checked; } .control_group .solo:hover { background-color: @solo_bgcolor_hover; } .control_group .solo:checked { background-color: @solo_bgcolor_checked; } .control_group .prefader:hover { background-color: @prefader_bgcolor_hover; } .control_group .prefader:checked { background-color: @prefader_bgcolor_checked; } /* Peak meters */ .over_zero { background-color: #cc4c00; } .is_nan { background-color: #b20000; } """ COLOR_TMPL_CSS = """\ .{} {{ background-color: {}; color: {}; }} """ def add_css_provider(css, priority=Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION): css_provider = Gtk.CssProvider() css_provider.load_from_data(css.encode("utf-8")) context = Gtk.StyleContext() screen = Gdk.Screen.get_default() context.add_provider_for_screen(screen, css_provider, priority) def get_text_color(background_color): """Calculate the luminance of the given color (GdkRGBA) and return an appropriate text color.""" # luminance coefficients taken from section C-9 from # http://www.faqs.org/faqs/graphics/colorspace-faq/ brightess = ( background_color.red * 0.212671 + background_color.green * 0.715160 + background_color.blue * 0.072169 ) if brightess > 0.5: return "black" else: return "white" def load_css_styles(): add_css_provider(DEFAULT_CSS) def set_background_color(widget, name, color): color_string = color.to_string() add_css_provider(COLOR_TMPL_CSS.format(name, color_string, get_text_color(color))) widget_context = widget.get_style_context() widget_context.add_class(name) def random_color(): return Gdk.RGBA(random(), random(), random(), 1) jack_mixer-release-17/jack_mixer/version.py.in000066400000000000000000000000321413224161500216070ustar00rootroot00000000000000__version__ = "@VERSION@" jack_mixer-release-17/meson.build000066400000000000000000000042331413224161500172000ustar00rootroot00000000000000project( 'jack_mixer', 'c', version: '17', license: 'GPL2+', default_options: [ 'warning_level=2' ], meson_version: '>=0.53.0' ) # Dependencies cc = meson.get_compiler('c') glib_dep = dependency('glib-2.0') math_dep = cc.find_library('m', required: false) jack2_dep = dependency('jack', version:'>=1.9.11', required: false) jack1_dep = dependency('jack', version:'>=0.125.0, <1.0', required: false) if not jack2_dep.found() and jack1_dep.found() jack_dep = jack1_dep elif jack2_dep.found() jack_dep = jack2_dep else error('No recent enough (jack2>=1.9.11 or jack1>=0.125.0) version of JACK found.') endif if get_option('gui').disabled() and get_option('jack-midi').disabled() error('Disabling both GUI and JACK-MIDI is not supported.') endif if get_option('gui').enabled() pymod = import('python') python = pymod.find_installation( 'python3', required: true, modules: get_option('check-py-modules') ? ['gi', 'cairo', 'xdg'] : [] ) endif # Installation directories prefix = get_option('prefix') bindir = join_paths(prefix, get_option('bindir')) datadir = join_paths(prefix, get_option('datadir')) #localedir = join_paths(prefix, get_option('localedir')) #pythondir = join_paths(prefix, python.get_path('purelib')) desktopdir = join_paths(datadir, 'applications') icondir = join_paths(datadir, 'icons', 'hicolor') raysessiondir = join_paths('/', 'etc', 'xdg', 'raysession', 'client_templates', '35_jackmixer') # Build jack_mix_box and generate _jack_mixer extension source subdir('src') # Build & install C extension module and Python package if get_option('gui').enabled() subdir('jack_mixer') endif # Install desktop file and icons if get_option('gui').enabled() subdir('data') endif # Install documentation subdir('docs') if get_option('gui').enabled() and not get_option('wheel') meson.add_install_script('meson_postinstall.py') endif summary({ 'Build jack_mixer GUI': get_option('gui').enabled(), 'JACK MIDI support': get_option('jack-midi').enabled(), 'Debug messages (verbose)': get_option('verbose'), 'Build for wheel': get_option('wheel'), }, section: 'Configuration') jack_mixer-release-17/meson_options.txt000066400000000000000000000012041413224161500204660ustar00rootroot00000000000000option('gui', type: 'feature', value: 'enabled', description: 'Enable GUI (disable to only build jack-mix_box)' ) option('jack-midi', type: 'feature', value: 'enabled', description: 'Enable JACK MIDI support' ) option('check-py-modules', type: 'boolean', value: true, description: 'Check whether required Python modules are installed' ) option('verbose', type: 'boolean', value: false, description: 'Turn on debug logging (for development)' ) option('wheel', type: 'boolean', value: false, description: 'Turn on build mode for creating a Python wheel (should not be used directly)' ) jack_mixer-release-17/meson_postinstall.py000077500000000000000000000012351413224161500211670ustar00rootroot00000000000000#!/usr/bin/env python3 import sysconfig from compileall import compile_dir from os import environ, path prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local') datadir = path.join(prefix, 'share') destdir = environ.get('MESON_INSTALL_DESTDIR_PREFIX', '') # Package managers set this so we don't need to run if 'DESTDIR' not in environ: from subprocess import call print('Updating icon cache...') call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')]) print('Compiling Python module to bytecode...') moduledir = sysconfig.get_path('purelib', vars={'base': destdir}) compile_dir(path.join(moduledir, 'jack_mixer'), optimize=1) jack_mixer-release-17/pyproject.toml000066400000000000000000000036651413224161500177620ustar00rootroot00000000000000[build-system] # https://thiblahute.gitlab.io/mesonpep517/ build-backend = "mesonpep517.buildapi" requires = [ "cython", "docutils", "wheel", "mesonpep517>=0.2", "ninja" ] [tool.mesonpep517.entry-points] console_scripts = [ "jack_mixer = jack_mixer.app:main" ] [tool.mesonpep517.metadata] summary = "A GTK+ JACK audio mixer application" description-file = "README.md" # 'keywords' metadata field not supported by mesonpep517 (yet) #keywords = "mixer,audio,music,jack,gtk" license = "GPL2+" author = "Nedko Arnaudov" author-email = "nedko (a.t) arnaudov (dot) name" maintainer = "Christopher Arndt" maintainer-email = "info (a.t.) chrisarndt.de" home-page = "https://rdio.space/jackmixer/" project-urls = [ "Source, https://github.com/jack-mixer/jack-mixer", ] requires= [ "PyGObject", "pycairo", "pyxdg", ] requires-python = ">=3.6" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: X11 Applications :: GTK", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Multimedia :: Sound/Audio :: Mixers", ] meson-python-option-name = "python3" meson-options = [ "-Dwheel=true", "--buildtype=release" ] [tool.isort] ensure_newline_before_comments = true force_grid_wrap = 0 include_trailing_comma = true line_length = 99 multi_line_output = 3 use_parentheses = true [tool.black] line-length = 99 target-version = ['py36', 'py37', 'py38'] force-exclude = 'jack_mixer/nsmclient\.py' jack_mixer-release-17/src/000077500000000000000000000000001413224161500156235ustar00rootroot00000000000000jack_mixer-release-17/src/_jack_mixer.pxd000066400000000000000000000155451413224161500206250ustar00rootroot00000000000000# # _jack_mixer.pxd # # cython: language_level=3 # from libcpp cimport bool cdef extern from "jack_mixer.h": # scale.h ctypedef void * jack_mixer_scale_t; cdef jack_mixer_scale_t scale_create() cdef bool scale_add_threshold(jack_mixer_scale_t scale, float db, float scale_value) cdef void scale_remove_thresholds(jack_mixer_scale_t scale) cdef void scale_calculate_coefficients(jack_mixer_scale_t scale) cdef double scale_db_to_scale(jack_mixer_scale_t scale, double db) cdef double scale_scale_to_db(jack_mixer_scale_t scale, double scale_value) cdef void scale_destroy(jack_mixer_scale_t scale) # jack_mixer.h ctypedef void * jack_mixer_t ctypedef void * jack_mixer_channel_t ctypedef void * jack_mixer_output_channel_t ctypedef void * jack_mixer_threshold_t ctypedef void (*midi_change_callback)(void *) cdef enum midi_behavior_mode: pass cdef enum meter_mode: pass ctypedef enum jack_mixer_error_t: pass cdef jack_mixer_error_t jack_mixer_error(); cdef const char* jack_mixer_error_str(); # mixer cdef jack_mixer_t mixer_create "create" (const char * jack_client_name_ptr, bool stereo) cdef void mixer_destroy "destroy" (jack_mixer_t mixer) cdef unsigned int mixer_get_channels_count "get_channels_count" (jack_mixer_t mixer) cdef const char * mixer_get_client_name "get_client_name" (jack_mixer_t mixer) cdef int mixer_get_last_midi_cc "get_last_midi_cc" (jack_mixer_t mixer) cdef void mixer_set_last_midi_cc "set_last_midi_cc" ( jack_mixer_t mixer, int new_channel) cdef int mixer_get_midi_behavior_mode "get_midi_behavior_mode" (jack_mixer_t mixer) cdef void mixer_set_midi_behavior_mode "set_midi_behavior_mode" ( jack_mixer_t mixer, midi_behavior_mode mode) cdef jack_mixer_channel_t mixer_add_channel "add_channel" ( jack_mixer_t mixer, const char * channel_name, bool stereo) cdef jack_mixer_output_channel_t mixer_add_output_channel "add_output_channel" ( jack_mixer_t mixer, const char * channel_name, bool stereo, bool system) cdef bool mixer_get_kmetering "get_kmetering" (jack_mixer_t mixer) cdef void mixer_set_kmetering "set_kmetering" (jack_mixer_t mixer, bool flag) # not used by Python #cdef void channels_volumes_read(jack_mixer_t mixer) #cdef void remove_channels(jack_mixer_t mixer) # channel cdef const char * channel_get_name(jack_mixer_channel_t channel) cdef int channel_rename(jack_mixer_channel_t channel, const char * name) cdef double channel_abspeak_read(jack_mixer_channel_t channel, meter_mode mode) cdef void channel_abspeak_reset(jack_mixer_channel_t channel, meter_mode mode) cdef void channel_mono_meter_read( jack_mixer_channel_t channel, double * mono_ptr, meter_mode mode) cdef void channel_stereo_meter_read( jack_mixer_channel_t channel, double * left_ptr, double * right_ptr, meter_mode mode) cdef void channel_mono_kmeter_read( jack_mixer_channel_t channel, double * left_ptr, double * left_rms_ptr, meter_mode mode) cdef void channel_stereo_kmeter_read( jack_mixer_channel_t channel, double * left_ptr, double * right_ptr, double * left_rms_ptr, double * right_rms_ptr, meter_mode mode) cdef void channel_mono_kmeter_reset(jack_mixer_channel_t channel) cdef void channel_stereo_kmeter_reset(jack_mixer_channel_t channel) cdef void channel_volume_write(jack_mixer_channel_t channel, double volume) cdef double channel_volume_read(jack_mixer_channel_t channel) cdef double channel_balance_read(jack_mixer_channel_t channel) cdef void channel_balance_write(jack_mixer_channel_t channel, double balance) cdef bool channel_is_out_muted(jack_mixer_channel_t channel) cdef void channel_out_mute(jack_mixer_channel_t channel) cdef void channel_out_unmute(jack_mixer_channel_t channel) cdef bool channel_is_soloed(jack_mixer_channel_t channel) cdef void channel_solo(jack_mixer_channel_t channel) cdef void channel_unsolo(jack_mixer_channel_t channel) cdef bool channel_is_stereo(jack_mixer_channel_t channel) cdef void channel_set_midi_change_callback( jack_mixer_channel_t channel, midi_change_callback callback, void * user_data) cdef bool channel_get_midi_in_got_events(jack_mixer_channel_t channel) cdef int channel_autoset_balance_midi_cc(jack_mixer_channel_t channel) cdef int channel_autoset_mute_midi_cc(jack_mixer_channel_t channel) cdef int channel_autoset_solo_midi_cc(jack_mixer_channel_t channel) cdef int channel_autoset_volume_midi_cc(jack_mixer_channel_t channel) cdef int channel_get_balance_midi_cc(jack_mixer_channel_t channel) cdef int channel_get_mute_midi_cc(jack_mixer_channel_t channel) cdef int channel_get_solo_midi_cc(jack_mixer_channel_t channel) cdef int channel_get_volume_midi_cc(jack_mixer_channel_t channel) cdef int channel_set_balance_midi_cc(jack_mixer_channel_t channel, int new_cc) cdef int channel_set_mute_midi_cc(jack_mixer_channel_t channel, int new_cc) cdef int channel_set_solo_midi_cc(jack_mixer_channel_t channel, int new_cc) cdef int channel_set_volume_midi_cc(jack_mixer_channel_t channel, int new_cc) cdef void channel_set_midi_scale(jack_mixer_channel_t channel, jack_mixer_scale_t scale) cdef void channel_set_midi_cc_balance_picked_up(jack_mixer_channel_t channel, bool status) cdef void channel_set_midi_cc_volume_picked_up(jack_mixer_channel_t channel, bool status) cdef void remove_channel(jack_mixer_channel_t channel) # output channel cdef bool output_channel_is_muted( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel) cdef void output_channel_set_muted( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel, bool muted_value) cdef bool output_channel_is_prefader( jack_mixer_output_channel_t output_channel) cdef void output_channel_set_prefader( jack_mixer_output_channel_t output_channel, bool pfl_value) cdef bool output_channel_is_in_prefader( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t input_channel) cdef void output_channel_set_in_prefader( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t input_channel, bool prefader_value) cdef bool output_channel_is_solo( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel) cdef void output_channel_set_solo( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel, bool solo_value) cdef void remove_output_channel(jack_mixer_output_channel_t output_channel) jack_mixer-release-17/src/_jack_mixer.pyx000066400000000000000000000421401413224161500206410ustar00rootroot00000000000000# # cython: language_level=3 # """Python bindings for jack_mixer.c and scale.c using Cython.""" __all__ = ("Scale", "MidiBehaviour", "Mixer") import enum from _jack_mixer cimport * cdef void midi_change_callback_func(void *userdata) with gil: """Wrapper for a Python callback function for MIDI input.""" channel = userdata channel._midi_change_callback() class MidiBehaviour(enum.IntEnum): """MIDI control behaviour. `JUMP_TO_VALUE` Received MIDI control messages affect mixer directly. `PICK_UP` Received MIDI control messages have to match up with current mixer value first (within a small margin), before further changes take effect. """ JUMP_TO_VALUE = 0 PICK_UP = 1 class MeterMode(enum.IntEnum): """Choose between pre-fader or post-fader metering. `PRE_FADER` Meter signal before applying fader. `POST_FADER` Meter signal after applying fader. """ PRE_FADER = 0 POST_FADER = 1 cdef class Scale: """Mixer level scale representation. Wraps `jack_mixer_scale_t` struct. """ cdef jack_mixer_scale_t _scale def __cinit__(self): self._scale = scale_create() def __dealloc__(self): if self._scale: scale_destroy(self._scale) cpdef bool add_threshold(self, float db, float scale_value): """Add scale treshold.""" return scale_add_threshold(self._scale, db, scale_value) cpdef void remove_thresholds(self): """Remove scale threshold.""" scale_remove_thresholds(self._scale) cpdef void calculate_coefficients(self): """Calculate scale coefficents.""" scale_calculate_coefficients(self._scale) cpdef double db_to_scale(self, double db): """Return scale value responding to given dB value.""" return scale_db_to_scale(self._scale, db) cpdef double scale_to_db(self, double scale_value): """Return dB value responding to given scale value.""" return scale_scale_to_db(self._scale, scale_value) cdef class Mixer: """Jack Mixer representation. Wraps `jack_mixer_t` struct. """ cdef jack_mixer_t _mixer cdef bool _stereo def __cinit__(self, name, stereo=True): self._stereo = stereo self._mixer = mixer_create(name.encode('utf-8'), stereo) if self._mixer == NULL: raise RuntimeError(jack_mixer_error_str().decode('utf-8')) def __dealloc__(self): if self._mixer: mixer_destroy(self._mixer) def destroy(self): """Close mixer Jack client and destroy mixer instance. The instance must not be used anymore after calling this method. """ if self._mixer: mixer_destroy(self._mixer) @property def channels_count(self): """Number of mixer channels.""" return mixer_get_channels_count(self._mixer) @property def client_name(self): """Jack client name of mixer.""" return mixer_get_client_name(self._mixer).decode('utf-8') @property def last_midi_cc(self): """Last received MIDI control change message.""" return mixer_get_last_midi_cc(self._mixer) @last_midi_cc.setter def last_midi_cc(self, int channel): mixer_set_last_midi_cc(self._mixer, channel) @property def midi_behavior_mode(self): """MIDI control change behaviour mode. See `MidiBehaviour` enum for more information. """ return MidiBehaviour(mixer_get_midi_behavior_mode(self._mixer)) @midi_behavior_mode.setter def midi_behavior_mode(self, mode): mixer_set_midi_behavior_mode(self._mixer, mode.value if isinstance(mode, MidiBehaviour) else mode) cpdef add_channel(self, channel_name, stereo=None): """Add a stereo or mono input channel with given name to the mixer. Returns a `Channel` instance when successfull or `None` if channel creation failed. """ cdef jack_mixer_channel_t chan_ptr if stereo is None: stereo = self._stereo chan_ptr = mixer_add_channel(self._mixer, channel_name.encode('utf-8'), stereo) if chan_ptr == NULL: return None return Channel.new(chan_ptr) cpdef add_output_channel(self, channel_name, stereo=None, system=False): """Add a stereo or mono output channel with given name to the mixer. Returns a `OutputChannel` instance when successfull or `None` if channel creation failed. """ cdef jack_mixer_output_channel_t chan_ptr if stereo is None: stereo = self._stereo chan_ptr = mixer_add_output_channel(self._mixer, channel_name.encode('utf-8'), stereo, system) if chan_ptr == NULL: return None return OutputChannel.new(chan_ptr) @property def kmetering(self): """Using kmeters.""" return mixer_get_kmetering(self._mixer) @kmetering.setter def kmetering(self, bool flag): mixer_set_kmetering(self._mixer, flag) cdef class Channel: """Jack Mixer (input) channel representation. Wraps `jack_mixer_channel_t` struct. """ cdef jack_mixer_channel_t _channel cdef object _midi_change_callback def __init__(self): raise TypeError("Channel instances can only be created via Mixer.add_channel().") @staticmethod cdef Channel new(jack_mixer_channel_t chan_ptr): """Create a new Channel instance. A pointer to an initialized `jack_mixer_channel_t` struct must be passed in. This should not be called directly but only via `Mixer.add_channel()`. """ cdef Channel channel = Channel.__new__(Channel) channel._channel = chan_ptr return channel @property def abspeak_postfader(self): """Absolute peak of channel meter. Set to `None` to reset the absolute peak to -inf. Trying to set it to any other value will raise a `ValueError`. """ return channel_abspeak_read(self._channel, MeterMode.POST_FADER) @abspeak_postfader.setter def abspeak_postfader(self, reset): if reset is not None: raise ValueError("abspeak can only be reset (set to None)") channel_abspeak_reset(self._channel, MeterMode.POST_FADER) @property def abspeak_prefader(self): """Absolute peak of channel meter. Set to `None` to reset the absolute peak to -inf. Trying to set it to any other value will raise a `ValueError`. """ return channel_abspeak_read(self._channel, MeterMode.PRE_FADER) @abspeak_prefader.setter def abspeak_prefader(self, reset): if reset is not None: raise ValueError("abspeak can only be reset (set to None)") channel_abspeak_reset(self._channel, MeterMode.PRE_FADER) @property def balance(self): """Channel balance property.""" return channel_balance_read(self._channel) @balance.setter def balance(self, double bal): channel_balance_write(self._channel, bal) @property def is_stereo(self): """Is channel stereo or mono?""" return channel_is_stereo(self._channel) @property def kmeter_prefader(self): """Read channel prefader kmeter. If channel is stereo, return a four-item tupel with ``(rms_left, rms_right, peak_left, peak_right)`` value. If channel is mono, return a tow-item tupel with ``(rms, peak)`` value. """ cdef double peak_left, peak_right, left_rms, right_rms if channel_is_stereo(self._channel): channel_stereo_kmeter_read( self._channel, &peak_left, &peak_right, &left_rms, &right_rms, MeterMode.PRE_FADER) return (left_rms, right_rms, peak_left, peak_right) else: channel_mono_kmeter_read(self._channel, &peak_left, &left_rms, MeterMode.PRE_FADER) return (left_rms, peak_left) @property def kmeter_postfader(self): """Read channel postfader kmeter. If channel is stereo, return a four-item tupel with ``(rms_left, rms_right, peak_left, peak_right)`` value. If channel is mono, return a tow-item tupel with ``(rms, peak)`` value. """ cdef double peak_left, peak_right, left_rms, right_rms if channel_is_stereo(self._channel): channel_stereo_kmeter_read( self._channel, &peak_left, &peak_right, &left_rms, &right_rms, MeterMode.POST_FADER) return (left_rms, right_rms, peak_left, peak_right) else: channel_mono_kmeter_read(self._channel, &peak_left, &left_rms, MeterMode.POST_FADER) return (left_rms, peak_left) def kmeter_reset(self): """Reset channel kmeters""" if channel_is_stereo(self._channel): channel_stereo_kmeter_reset(self._channel) else: channel_mono_kmeter_reset(self._channel) @property def meter_prefader(self): """Read channel meter. If channel is stereo, return a two-item tupel with (left, right) value. If channel is mono, return a tupel with the value as the only item. """ cdef double left, right if channel_is_stereo(self._channel): channel_stereo_meter_read(self._channel, &left, &right, MeterMode.PRE_FADER) return (left, right) else: channel_mono_meter_read(self._channel, &left, MeterMode.PRE_FADER) return (left,) @property def meter_postfader(self): """Read channel meter. If channel is stereo, return a two-item tupel with (left, right) value. If channel is mono, return a tupel with the value as the only item. """ cdef double left, right if channel_is_stereo(self._channel): channel_stereo_meter_read(self._channel, &left, &right, MeterMode.POST_FADER) return (left, right) else: channel_mono_meter_read(self._channel, &left, MeterMode.POST_FADER) return (left,) @property def midi_change_callback(self): """Function to be called when a channel property is changed via MIDI. The callback function takes no arguments. Assign `None` to remove any existing callback. """ return self._midi_change_callback @midi_change_callback.setter def midi_change_callback(self, callback): self._midi_change_callback = callback if callback is None: channel_set_midi_change_callback(self._channel, NULL, NULL) else: channel_set_midi_change_callback(self._channel, &midi_change_callback_func, self) @property def name(self): """Channel name property.""" return channel_get_name(self._channel).decode('utf-8') @name.setter def name(self, newname): if channel_rename(self._channel, newname.encode('utf-8')) != 0: raise RuntimeError(jack_mixer_error_str().decode('utf-8')) @property def out_mute(self): """Channel solo status property.""" return channel_is_out_muted(self._channel) @out_mute.setter def out_mute(self, bool value): if value: channel_out_mute(self._channel) else: channel_out_unmute(self._channel) @property def solo(self): """Channel solo status property.""" return channel_is_soloed(self._channel) @solo.setter def solo(self, bool value): if value: channel_solo(self._channel) else: channel_unsolo(self._channel) @property def midi_in_got_events(self): """Did channel receive any MIDI events assigned to one of its controls? Reading this property also resets it to False. """ return channel_get_midi_in_got_events(self._channel) @property def midi_scale(self): """MIDI scale used by channel.""" raise AttributeError("midi_scale can only be set.") @midi_scale.setter def midi_scale(self, Scale scale): channel_set_midi_scale(self._channel, scale._scale) @property def volume(self): """Channel volume property.""" return channel_volume_read(self._channel) @volume.setter def volume(self, double vol): channel_volume_write(self._channel, vol) @property def balance_midi_cc(self): """MIDI CC assigned to control channel balance.""" return channel_get_balance_midi_cc(self._channel) @balance_midi_cc.setter def balance_midi_cc(self, int cc): if channel_set_balance_midi_cc(self._channel, cc) != 0: raise ValueError(jack_mixer_error_str().decode('utf-8')) @property def mute_midi_cc(self): """MIDI CC assigned to control channel mute status.""" return channel_get_mute_midi_cc(self._channel) @mute_midi_cc.setter def mute_midi_cc(self, int cc): if channel_set_mute_midi_cc(self._channel, cc) != 0: raise ValueError(jack_mixer_error_str().decode('utf-8')) @property def solo_midi_cc(self): """MIDI CC assigned to control channel solo status.""" return channel_get_solo_midi_cc(self._channel) @solo_midi_cc.setter def solo_midi_cc(self, int cc): if channel_set_solo_midi_cc(self._channel, cc) != 0: raise ValueError(jack_mixer_error_str().decode('utf-8')) @property def volume_midi_cc(self): """MIDI CC assigned to control channel volume.""" return channel_get_volume_midi_cc(self._channel) @volume_midi_cc.setter def volume_midi_cc(self, int cc): if channel_set_volume_midi_cc(self._channel, cc) != 0: raise ValueError(jack_mixer_error_str().decode('utf-8')) def autoset_balance_midi_cc(self): """Auto assign MIDI CC for channel balance.""" return channel_autoset_balance_midi_cc(self._channel) def autoset_mute_midi_cc(self): """Auto assign MIDI CC for channel mute status.""" return channel_autoset_mute_midi_cc(self._channel) def autoset_solo_midi_cc(self): """Auto assign MIDI CC for channel solo status.""" return channel_autoset_solo_midi_cc(self._channel) def autoset_volume_midi_cc(self): """Auto assign MIDI CC for channel volume.""" return channel_autoset_volume_midi_cc(self._channel) def remove(self): """Remove channel.""" remove_channel(self._channel) def set_midi_cc_balance_picked_up(self, bool status): """Set whether balance value is out-of-sync with MIDI control.""" channel_set_midi_cc_balance_picked_up(self._channel, status) def set_midi_cc_volume_picked_up(self, bool status): """Set whether volume value is out-of-sync with MIDI control.""" channel_set_midi_cc_volume_picked_up(self._channel, status) cdef class OutputChannel(Channel): """Jack Mixer output channel representation. Wraps `jack_mixer_output_channel_t` struct. Inherits from `Channel` class. """ cdef jack_mixer_output_channel_t _output_channel def __init__(self): raise TypeError("OutputChannel instances can only be created via " "Mixer.add_output_channel().") @staticmethod cdef OutputChannel new(jack_mixer_output_channel_t chan_ptr): """Create a new OutputChannel instance. A pointer to an initialzed `jack_mixer_output_channel_t` struct must be passed in. This should not be called directly but only via `Mixer.add_output_channel()`. """ cdef OutputChannel channel = OutputChannel.__new__(OutputChannel) channel._output_channel = chan_ptr channel._channel = chan_ptr return channel @property def prefader(self): return output_channel_is_prefader(self._output_channel) @prefader.setter def prefader(self, bool pfl): output_channel_set_prefader(self._output_channel, pfl) def is_in_prefader(self, Channel channel): """Is a channel set as prefader?""" return output_channel_is_in_prefader(self._output_channel, channel._channel) def set_in_prefader(self, Channel channel, bool value): """Set a channel as prefader.""" output_channel_set_in_prefader(self._output_channel, channel._channel, value) def is_muted(self, Channel channel): """Is a channel set as muted?""" return output_channel_is_muted(self._output_channel, channel._channel) def set_muted(self, Channel channel, bool value): """Set a channel as muted.""" output_channel_set_muted(self._output_channel, channel._channel, value) def is_solo(self, Channel channel): """Is a channel set as solo?""" return output_channel_is_solo(self._output_channel, channel._channel) def set_solo(self, Channel channel, bool value): """Set a channel as solo.""" output_channel_set_solo(self._output_channel, channel._channel, value) def remove(self): """Remove output channel.""" remove_output_channel(self._output_channel) jack_mixer-release-17/src/jack_mix_box.c000066400000000000000000000122051413224161500204240ustar00rootroot00000000000000/***************************************************************************** * * This file is part of jack_mixer * * Copyright (C) 2006 Nedko Arnaudov * Copyright (C) 2009-2011 Frederic Peters * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * *****************************************************************************/ /* * jack_mix_box is a most minimalistic jack mixer, a set of mono/sterero input * channels, mixed to a single output channel, with the volume of the * input channels controlled by MIDI control change (CC) codes. * */ #include #include #include #include #include #include #include #include #include #include "jack_mixer.h" #define _(String) gettext(String) jack_mixer_t mixer; bool keepRunning = true; void usage() { const char* _usage = _( "Usage: " "jack_mix_box [-n ] [-p] [-s] [-v ] MIDI_CC...\n" "\n" "-h|--help print this help message\n" "-n|--name set JACK client name\n" "-p|--pickup enable MIDI pickup mode (default: jump-to-value)\n" "-s|--stereo make all input channels stereo with left+right input\n" "-v|--volume initial volume gain in dBFS (default 0.0, i.e. unity gain)\n" "\n" "Each positional argument is interpreted as a MIDI Control Change number and\n" "adds a mixer channel with one (mono) or left+right (stereo) inputs, whose\n" "volume can be controlled via the given MIDI Control Change.\n" "\n" "Send SIGUSR1 to the process to have the current volumes reported per input\n" "channel.\n\n"); fputs(_usage, stdout); } void reportVolume(int sig) { (void)sig; channels_volumes_read(mixer); } void triggerShutDown(int sig) { (void)sig; keepRunning = false; } int main(int argc, char *argv[]) { jack_mixer_scale_t scale; jack_mixer_channel_t main_mix_channel; char *jack_cli_name = NULL; int channel_index; bool bStereo = false; enum midi_behavior_mode ePickup = Jump_To_Value; double initialVolume = 0.0f; //in dbFS char * localedir; localedir = getenv("LOCALEDIR"); setlocale(LC_ALL, ""); bindtextdomain("jack_mixer", localedir != NULL ? localedir : LOCALEDIR); textdomain("jack_mixer"); while (1) { int c; static struct option long_options[] = { {"name", required_argument, 0, 'n'}, {"help", no_argument, 0, 'h'}, {"pickup", no_argument, 0, 'p'}, {"stereo", no_argument, 0, 's'}, {"volume", required_argument, 0, 'v'}, {0, 0, 0, 0} }; int option_index = 0; c = getopt_long (argc, argv, "sphn:v:", long_options, &option_index); if (c == -1) break; switch (c) { case 'n': jack_cli_name = strdup(optarg); break; case 's': bStereo = true; break; case 'v': initialVolume = strtod(optarg, NULL); break; case 'h': usage(); exit(0); break; case 'p': ePickup = Pick_Up; break; default: fprintf(stderr, _("Unknown argument, aborting.\n")); exit(1); } } if (optind == argc) { fputs(_("You must specify at least one input channel.\n"), stderr); exit(1); } scale = scale_create(); scale_add_threshold(scale, -70.0, 0.0); scale_add_threshold(scale, 0.0, 1.0); scale_calculate_coefficients(scale); if (jack_cli_name == NULL) { jack_cli_name = strdup("jack_mix_box"); } mixer = create(jack_cli_name, false); if (mixer == NULL) { fputs(jack_mixer_error_str(), stderr); return -1; } main_mix_channel = add_output_channel(mixer, "MAIN", true, false); channel_set_midi_scale(main_mix_channel, scale); channel_volume_write(main_mix_channel, 0.0); set_midi_behavior_mode(mixer, ePickup); channel_index = 0; while (optind < argc) { char *channel_name; jack_mixer_channel_t channel; channel_index += 1; channel_name = malloc(15); if (snprintf(channel_name, 15, "Channel %d", channel_index) >= 15) { free(channel_name); abort(); } channel = add_channel(mixer, channel_name, bStereo); if (channel == NULL) { fprintf(stderr, _("Failed to add channel %d, aborting.\n"), channel_index); exit(1); } channel_set_volume_midi_cc(channel, atoi(argv[optind++])); channel_set_midi_scale(channel, scale); channel_volume_write(channel, initialVolume); free(channel_name); } signal(SIGUSR1, reportVolume); signal(SIGTERM, triggerShutDown); signal(SIGHUP, triggerShutDown); signal(SIGINT, triggerShutDown); while (keepRunning) { usleep(500u * 1000u); //500msec } remove_channels(mixer); remove_output_channel(main_mix_channel); destroy(mixer); scale_destroy(scale); free(jack_cli_name); return 0; } jack_mixer-release-17/src/jack_mixer.c000066400000000000000000002127611413224161500201140ustar00rootroot00000000000000/* -*- Mode: C ; c-basic-offset: 2 -*- */ /***************************************************************************** * * This file is part of jack_mixer * * Copyright (C) 2006 Nedko Arnaudov * Copyright (C) 2009 Frederic Peters * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * *****************************************************************************/ #include #include #include #include #include #include #include #include #include #if defined(HAVE_JACK_MIDI) #include #endif #include #include #include #include "jack_mixer.h" #include "log.h" #define _(String) String struct kmeter { float _z1; // filter state float _z2; // filter state float _rms; // max rms value since last read() float _dpk; // current digital peak value int _cnt; // digital peak hold counter bool _flag; // flag set by read(), resets _rms int _hold; // number of JACK periods to hold peak value float _fall; // per period fallback multiplier for peak value float _omega; // ballistics filter constant. }; struct channel { struct jack_mixer * mixer_ptr; char * name; bool stereo; bool out_mute; float volume_transition_seconds; unsigned int num_volume_transition_steps; float volume; jack_nframes_t volume_idx; float volume_new; float balance; jack_nframes_t balance_idx; float balance_new; float volume_left; float volume_left_new; float volume_right; float volume_right_new; float meter_left_postfader; float meter_left_prefader; float meter_right_postfader; float meter_right_prefader; float abspeak_postfader; float abspeak_prefader; struct kmeter kmeter_left; struct kmeter kmeter_right; struct kmeter kmeter_prefader_left; struct kmeter kmeter_prefader_right; jack_port_t * port_left; jack_port_t * port_right; jack_nframes_t peak_frames; float peak_left_prefader; float peak_left_postfader; float peak_right_prefader; float peak_right_postfader; jack_default_audio_sample_t * tmp_mixed_frames_left; jack_default_audio_sample_t * tmp_mixed_frames_right; jack_default_audio_sample_t * frames_left; jack_default_audio_sample_t * frames_right; jack_default_audio_sample_t * prefader_frames_left; jack_default_audio_sample_t * prefader_frames_right; jack_default_audio_sample_t * left_buffer_ptr; jack_default_audio_sample_t * right_buffer_ptr; bool NaN_detected; int8_t midi_cc_volume_index; int8_t midi_cc_balance_index; int8_t midi_cc_mute_index; int8_t midi_cc_solo_index; bool midi_cc_volume_picked_up; bool midi_cc_balance_picked_up; bool midi_in_got_events; int midi_out_has_events; void (*midi_change_callback) (void*); void *midi_change_callback_data; jack_mixer_scale_t midi_scale; }; struct output_channel { struct channel channel; GSList *soloed_channels; GSList *muted_channels; GSList *prefader_channels; bool system; /* system channel, without any associated UI */ bool prefader; }; struct jack_mixer { pthread_mutex_t mutex; jack_client_t * jack_client; GSList *input_channels_list; GSList *output_channels_list; GSList *soloed_channels; jack_port_t * port_midi_in; jack_port_t * port_midi_out; bool kmetering; int8_t last_midi_cc; enum midi_behavior_mode midi_behavior; struct channel* midi_cc_map[128]; }; static jack_mixer_output_channel_t create_output_channel( jack_mixer_t mixer, const char * channel_name, bool stereo, bool system); static inline void update_channel_buffers( struct channel * channel_ptr, jack_nframes_t nframes); static void unset_midi_cc_mapping( struct jack_mixer * mixer, int8_t cc); float value_to_db( float value) { if (value <= 0) { return -INFINITY; } return 20.0 * log10f(value); } float db_to_value( float db) { return powf(10.0, db / 20.0); } double interpolate( double start, double end, int step, int steps) { double ret; double frac = 0.01; LOG_DEBUG("Interpolation: start=%f -> end=%f, step=%d\n", start, end, step); if (start <= 0) { if (step <= frac * steps) { ret = frac * end * step / steps; } else { ret = db_to_value(value_to_db(frac * end) + (value_to_db(end) - value_to_db(frac * end)) * step / steps);; } } else if (end <= 0) { if (step >= (1 - frac) * steps) { ret = frac * start - frac * start * step / steps; } else { ret = db_to_value(value_to_db(start) + (value_to_db(frac * start) - value_to_db(start)) * step / steps); } } else { ret = db_to_value(value_to_db(start) + (value_to_db(end) - value_to_db(start)) *step /steps); } LOG_DEBUG("Interpolated value: %f\n", ret); return ret; } const char* const _jack_mixer_error_str[] = { /* JACK_MIXER_NO_ERROR */ _("No error.\n"), /* JACK_MIXER_ERROR_JACK_CLIENT_CREATE */ _("Could not create JACK client.\nPlease make sure JACK daemon is running.\n"), /* JACK_MIXER_ERROR_JACK_MIDI_IN_CREATE */ _("Could not create JACK MIDI in port.\n"), /* JACK_MIXER_ERROR_JACK_MIDI_OUT_CREATE */ _("Could not create JACK MIDI out port.\n"), /* JACK_MIXER_ERROR_JACK_SET_PROCESS_CALLBACK */ _("Could not set JACK process callback.\n"), /* JACK_MIXER_ERROR_JACK_SET_BUFFER_SIZE_CALLBACK */ _("Could not set JACK buffer size callback.\n"), /* JACK_MIXER_ERROR_JACK_ACTIVATE */ _("Could not activate JACK client.\n"), /* JACK_MIXER_ERROR_CHANNEL_MALLOC */ _("Could not allocate memory for channel.\n"), /* JACK_MIXER_ERROR_CHANNEL_NAME_MALLOC */ _("Could not allocate memory for channel name.\n"), /* JACK_MIXER_ERROR_PORT_REGISTER */ _("Could not register JACK port for channel.\n"), /* JACK_MIXER_ERROR_PORT_REGISTER_LEFT */ _("Could not register JACK port for left channel.\n"), /* JACK_MIXER_ERROR_PORT_REGISTER_RIGHT */ _("Could not register JACK port for right channel.\n"), /* JACK_MIXER_ERROR_JACK_RENAME_PORT */ _("Could not rename JACK port for channel.\n"), /* JACK_MIXER_ERROR_JACK_RENAME_PORT_LEFT */ _("Could not rename JACK port for left channel.\n"), /* JACK_MIXER_ERROR_JACK_RENAME_PORT_LEFT */ _("Could not rename JACK port for right channel.\n"), /* JACK_MIXER_ERROR_PORT_NAME_MALLOC */ _("Could not allocate memory for port name.\n"), /* JACK_MIXER_ERROR_INVALID_CC */ _("Control Change number out of range.\n"), /* JACK_MIXER_ERROR_NO_FREE_CC */ _("No free Control Change number.\n") }; jack_mixer_error_t _jack_mixer_error = JACK_MIXER_NO_ERROR; /*****************************************************************************/ /* Public API */ jack_mixer_error_t jack_mixer_error() { return _jack_mixer_error; } const char* jack_mixer_error_str() { const char* err_str = NULL; /* Ensure error codes are within valid range */ if (_jack_mixer_error == JACK_MIXER_NO_ERROR || _jack_mixer_error >= JACK_MIXER_ERROR_COUNT) { goto done; } err_str = gettext(_jack_mixer_error_str[_jack_mixer_error]); done: return err_str; } #define channel_ptr ((struct channel *)channel) const char* channel_get_name( jack_mixer_channel_t channel) { return channel_ptr->name; } int channel_rename( jack_mixer_channel_t channel, const char * name) { char * new_name; size_t channel_name_size; char * port_name; int ret; new_name = strdup(name); if (new_name == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_PORT_NAME_MALLOC; goto fail; } if (channel_ptr->name) { free(channel_ptr->name); } channel_ptr->name = new_name; if (channel_ptr->stereo) { channel_name_size = strlen(name); port_name = malloc(channel_name_size + 3); memcpy(port_name, name, channel_name_size); port_name[channel_name_size] = ' '; port_name[channel_name_size+1] = 'L'; port_name[channel_name_size+2] = 0; ret = jack_port_rename(channel_ptr->mixer_ptr->jack_client, channel_ptr->port_left, port_name); if (ret != 0) { _jack_mixer_error = JACK_MIXER_ERROR_JACK_RENAME_PORT_LEFT; goto fail; } port_name[channel_name_size+1] = 'R'; ret = jack_port_rename(channel_ptr->mixer_ptr->jack_client, channel_ptr->port_right, port_name); if (ret != 0) { _jack_mixer_error = JACK_MIXER_ERROR_JACK_RENAME_PORT_RIGHT; goto fail_free; } free(port_name); } else { ret = jack_port_rename(channel_ptr->mixer_ptr->jack_client, channel_ptr->port_left, name); if (ret != 0) { _jack_mixer_error = JACK_MIXER_ERROR_JACK_RENAME_PORT; goto fail; } } return 0; fail_free: free(port_name); fail: return -1; } bool channel_is_stereo( jack_mixer_channel_t channel) { return channel_ptr->stereo; } int8_t channel_get_balance_midi_cc( jack_mixer_channel_t channel) { return channel_ptr->midi_cc_balance_index; } /* * Remove assignment for given MIDI CC * * This is an internal (static) function */ static void unset_midi_cc_mapping( struct jack_mixer * mixer, int8_t cc) { struct channel *channel = mixer->midi_cc_map[cc]; if (!channel) { return; } if (channel->midi_cc_volume_index == cc) { channel->midi_cc_volume_index = -1; } else if (channel->midi_cc_balance_index == cc) { channel->midi_cc_balance_index = -1; } else if (channel->midi_cc_mute_index == cc) { channel->midi_cc_mute_index = -1; } else if (channel->midi_cc_solo_index == cc) { channel->midi_cc_solo_index = -1; } mixer->midi_cc_map[cc] = NULL; } int channel_set_balance_midi_cc( jack_mixer_channel_t channel, int8_t new_cc) { if (new_cc < 0) { _jack_mixer_error = JACK_MIXER_ERROR_INVALID_CC; return -1; } /* Remove previous assignment for this CC */ unset_midi_cc_mapping(channel_ptr->mixer_ptr, new_cc); /* Remove previous balance CC mapped to this channel (if any) */ if (channel_ptr->midi_cc_balance_index != -1) { channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_balance_index] = NULL; } channel_ptr->mixer_ptr->midi_cc_map[new_cc] = channel_ptr; channel_ptr->midi_cc_balance_index = new_cc; return 0; } int8_t channel_get_volume_midi_cc( jack_mixer_channel_t channel) { return channel_ptr->midi_cc_volume_index; } int channel_set_volume_midi_cc( jack_mixer_channel_t channel, int8_t new_cc) { if (new_cc < 0) { _jack_mixer_error = JACK_MIXER_ERROR_INVALID_CC; return -1; } /* remove previous assignment for this CC */ unset_midi_cc_mapping(channel_ptr->mixer_ptr, new_cc); /* remove previous volume CC mapped to this channel (if any) */ if (channel_ptr->midi_cc_volume_index != -1) { channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_volume_index] = NULL; } channel_ptr->mixer_ptr->midi_cc_map[new_cc] = channel_ptr; channel_ptr->midi_cc_volume_index = new_cc; return 0; } int8_t channel_get_mute_midi_cc( jack_mixer_channel_t channel) { return channel_ptr->midi_cc_mute_index; } int channel_set_mute_midi_cc( jack_mixer_channel_t channel, int8_t new_cc) { if (new_cc < 0) { _jack_mixer_error = JACK_MIXER_ERROR_INVALID_CC; return -1; } /* Remove previous assignment for this CC */ unset_midi_cc_mapping(channel_ptr->mixer_ptr, new_cc); /* Remove previous mute CC mapped to this channel (if any) */ if (channel_ptr->midi_cc_mute_index != -1) { channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_mute_index] = NULL; } channel_ptr->mixer_ptr->midi_cc_map[new_cc] = channel_ptr; channel_ptr->midi_cc_mute_index = new_cc; return 0; } int8_t channel_get_solo_midi_cc( jack_mixer_channel_t channel) { return channel_ptr->midi_cc_solo_index; } void channel_set_midi_cc_volume_picked_up( jack_mixer_channel_t channel, bool status) { LOG_DEBUG("Setting channel %s volume picked up to %d.\n", channel_ptr->name, status); channel_ptr->midi_cc_volume_picked_up = status; } void channel_set_midi_cc_balance_picked_up( jack_mixer_channel_t channel, bool status) { LOG_DEBUG("Setting channel %s balance picked up to %d.\n", channel_ptr->name, status); channel_ptr->midi_cc_balance_picked_up = status; } int channel_set_solo_midi_cc( jack_mixer_channel_t channel, int8_t new_cc) { if (new_cc < 0) { _jack_mixer_error = JACK_MIXER_ERROR_INVALID_CC; return -1; } /* Remove previous assignment for this CC */ unset_midi_cc_mapping(channel_ptr->mixer_ptr, new_cc); /* Remove previous solo CC mapped to this channel (if any) */ if (channel_ptr->midi_cc_solo_index != -1) { channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_solo_index] = NULL; } channel_ptr->mixer_ptr->midi_cc_map[new_cc] = channel_ptr; channel_ptr->midi_cc_solo_index = new_cc; return 0; } int channel_autoset_volume_midi_cc( jack_mixer_channel_t channel) { struct jack_mixer *mixer_ptr = channel_ptr->mixer_ptr; for (int i = 11 ; i < 128 ; i++) { if (!mixer_ptr->midi_cc_map[i]) { mixer_ptr->midi_cc_map[i] = channel_ptr; channel_ptr->midi_cc_volume_index = i; LOG_DEBUG("New channel \"%s\" volume mapped to CC#%i.\n", channel_ptr->name, i); return i; } } _jack_mixer_error = JACK_MIXER_ERROR_NO_FREE_CC; return -1; } int channel_autoset_balance_midi_cc( jack_mixer_channel_t channel) { struct jack_mixer *mixer_ptr = channel_ptr->mixer_ptr; for (int i = 11; i < 128 ; i++) { if (!mixer_ptr->midi_cc_map[i]) { mixer_ptr->midi_cc_map[i] = channel_ptr; channel_ptr->midi_cc_balance_index = i; LOG_DEBUG("New channel \"%s\" balance mapped to CC#%i.\n", channel_ptr->name, i); return i; } } _jack_mixer_error = JACK_MIXER_ERROR_NO_FREE_CC; return -1; } int channel_autoset_mute_midi_cc( jack_mixer_channel_t channel) { struct jack_mixer *mixer_ptr = channel_ptr->mixer_ptr; for (int i = 11; i < 128 ; i++) { if (!mixer_ptr->midi_cc_map[i]) { mixer_ptr->midi_cc_map[i] = channel_ptr; channel_ptr->midi_cc_mute_index = i; LOG_DEBUG("New channel \"%s\" mute mapped to CC#%i.\n", channel_ptr->name, i); return i; } } _jack_mixer_error = JACK_MIXER_ERROR_NO_FREE_CC; return -1; } int channel_autoset_solo_midi_cc( jack_mixer_channel_t channel) { struct jack_mixer *mixer_ptr = channel_ptr->mixer_ptr; for (int i = 11; i < 128 ; i++) { if (!mixer_ptr->midi_cc_map[i]) { mixer_ptr->midi_cc_map[i] = channel_ptr; channel_ptr->midi_cc_solo_index = i; LOG_DEBUG("New channel \"%s\" solo mapped to CC#%i.\n", channel_ptr->name, i); return i; } } _jack_mixer_error = JACK_MIXER_ERROR_NO_FREE_CC; return -1; } void remove_channel( jack_mixer_channel_t channel) { GSList *list_ptr; channel_ptr->mixer_ptr->input_channels_list = g_slist_remove( channel_ptr->mixer_ptr->input_channels_list, channel_ptr); free(channel_ptr->name); /* remove references to input channel from all output channels */ for (list_ptr = channel_ptr->mixer_ptr->output_channels_list; list_ptr; list_ptr = g_slist_next(list_ptr)) { struct output_channel *output_channel_ptr = list_ptr->data; output_channel_set_solo(output_channel_ptr, channel, false); output_channel_set_muted(output_channel_ptr, channel, false); } jack_port_unregister(channel_ptr->mixer_ptr->jack_client, channel_ptr->port_left); if (channel_ptr->stereo) { jack_port_unregister(channel_ptr->mixer_ptr->jack_client, channel_ptr->port_right); } if (channel_ptr->midi_cc_volume_index != -1) { assert(channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_volume_index] == channel_ptr); channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_volume_index] = NULL; } if (channel_ptr->midi_cc_balance_index != -1) { assert(channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_balance_index] == channel_ptr); channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_balance_index] = NULL; } if (channel_ptr->midi_cc_mute_index != -1) { assert(channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_mute_index] == channel_ptr); channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_mute_index] = NULL; } if (channel_ptr->midi_cc_solo_index != -1) { assert(channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_solo_index] == channel_ptr); channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_solo_index] = NULL; } free(channel_ptr->frames_left); free(channel_ptr->frames_right); free(channel_ptr->prefader_frames_left); free(channel_ptr->prefader_frames_right); free(channel_ptr); } void channel_stereo_meter_read( jack_mixer_channel_t channel, double * left_ptr, double * right_ptr, enum meter_mode mode) { assert(channel_ptr); if (mode == Pre_Fader) { *left_ptr = value_to_db(channel_ptr->meter_left_prefader); *right_ptr = value_to_db(channel_ptr->meter_right_prefader); } else { *left_ptr = value_to_db(channel_ptr->meter_left_postfader); *right_ptr = value_to_db(channel_ptr->meter_right_postfader); } } void channel_mono_meter_read( jack_mixer_channel_t channel, double * mono_ptr, enum meter_mode mode) { if (mode == Pre_Fader) { *mono_ptr = value_to_db(channel_ptr->meter_left_prefader); } else { *mono_ptr = value_to_db(channel_ptr->meter_left_postfader); } } void channel_stereo_kmeter_read( jack_mixer_channel_t channel, double * left_ptr, double * right_ptr, double * left_rms_ptr, double * right_rms_ptr, enum meter_mode mode) { struct kmeter *kmeter_left; struct kmeter *kmeter_right; assert(channel_ptr); if (mode == Pre_Fader) { kmeter_left = &channel_ptr->kmeter_prefader_left; kmeter_right = &channel_ptr->kmeter_prefader_right; } else { kmeter_left = &channel_ptr->kmeter_left; kmeter_right = &channel_ptr->kmeter_right; } *left_ptr = value_to_db(kmeter_left->_dpk); *right_ptr = value_to_db(kmeter_right->_dpk); *left_rms_ptr = value_to_db(kmeter_left->_rms); *right_rms_ptr = value_to_db(kmeter_right->_rms); kmeter_left->_flag = true; kmeter_right->_flag = true; } void channel_mono_kmeter_read( jack_mixer_channel_t channel, double * mono_ptr, double * mono_rms_ptr, enum meter_mode mode) { struct kmeter *kmeter; if (mode == Pre_Fader) { kmeter = &channel_ptr->kmeter_prefader_left; } else { kmeter = &channel_ptr->kmeter_left; } *mono_ptr = value_to_db(kmeter->_dpk); *mono_rms_ptr = value_to_db(kmeter->_rms); kmeter->_flag = true; } void channel_mono_kmeter_reset( jack_mixer_channel_t channel) { struct kmeter *kmeter; kmeter = &channel_ptr->kmeter_prefader_left; kmeter->_flag = true; kmeter = &channel_ptr->kmeter_left; kmeter->_flag = true; } void channel_stereo_kmeter_reset( jack_mixer_channel_t channel) { struct kmeter *kmeter; channel_mono_kmeter_reset(channel); kmeter = &channel_ptr->kmeter_prefader_right; kmeter->_flag = true; kmeter = &channel_ptr->kmeter_right; kmeter->_flag = true; } void channel_volume_write( jack_mixer_channel_t channel, double volume) { assert(channel_ptr); double value = db_to_value(volume); /*If changing volume and find we're in the middle of a previous transition, *then set current volume to place in transition to avoid a jump.*/ if (channel_ptr->volume_new != channel_ptr->volume) { channel_ptr->volume = interpolate(channel_ptr->volume, channel_ptr->volume_new, channel_ptr->volume_idx, channel_ptr->num_volume_transition_steps); } channel_ptr->volume_idx = 0; if (channel_ptr->volume_new != value) { channel_ptr->midi_out_has_events |= CHANNEL_VOLUME; } channel_ptr->volume_new = value; LOG_DEBUG("\"%s\" volume -> %f.\n", channel_ptr->name, value); } double channel_volume_read( jack_mixer_channel_t channel) { assert(channel_ptr); return value_to_db(channel_ptr->volume_new); } void channels_volumes_read( jack_mixer_t mixer_ptr) { GSList *node_ptr; struct channel *pChannel; struct jack_mixer * pMixer = (struct jack_mixer *)mixer_ptr; for (node_ptr = pMixer->input_channels_list; node_ptr; node_ptr = g_slist_next(node_ptr)) { pChannel = (struct channel *)node_ptr->data; double vol = channel_volume_read( (jack_mixer_channel_t)pChannel); printf(gettext("%s: volume is %f dbFS for mixer channel: %s\n"), jack_get_client_name(pMixer->jack_client), vol, pChannel->name); } } void channel_balance_write( jack_mixer_channel_t channel, double balance) { assert(channel_ptr); if (channel_ptr->balance != channel_ptr->balance_new) { channel_ptr->balance = channel_ptr->balance + channel_ptr->balance_idx * (channel_ptr->balance_new - channel_ptr->balance) / channel_ptr->num_volume_transition_steps; } channel_ptr->balance_idx = 0; if (channel_ptr->balance_new != balance) { channel_ptr->midi_out_has_events |= CHANNEL_BALANCE; } channel_ptr->balance_new = balance; LOG_DEBUG("\"%s\" balance -> %f\n", channel_ptr->name, balance); } double channel_balance_read( jack_mixer_channel_t channel) { assert(channel_ptr); return channel_ptr->balance_new; } double channel_abspeak_read( jack_mixer_channel_t channel, enum meter_mode mode) { assert(channel_ptr); if (channel_ptr->NaN_detected) { return sqrt(-1); } else { return value_to_db(mode == Post_Fader ? channel_ptr->abspeak_postfader : channel_ptr->abspeak_prefader); } } void channel_abspeak_reset( jack_mixer_channel_t channel, enum meter_mode mode) { if (mode == Post_Fader) { channel_ptr->abspeak_postfader = 0; } else if (mode == Pre_Fader) { channel_ptr->abspeak_prefader = 0; } channel_ptr->NaN_detected = false; } void channel_out_mute( jack_mixer_channel_t channel) { if (!channel_ptr->out_mute) { channel_ptr->out_mute = true; channel_ptr->midi_out_has_events |= CHANNEL_MUTE; LOG_DEBUG("\"%s\" muted.\n", channel_ptr->name); } } void channel_out_unmute( jack_mixer_channel_t channel) { if (channel_ptr->out_mute) { channel_ptr->out_mute = false; channel_ptr->midi_out_has_events |= CHANNEL_MUTE; LOG_DEBUG("\"%s\" un-muted.\n", channel_ptr->name); } } bool channel_is_out_muted( jack_mixer_channel_t channel) { return channel_ptr->out_mute; } void channel_solo( jack_mixer_channel_t channel) { if (g_slist_find(channel_ptr->mixer_ptr->soloed_channels, channel) != NULL) return; channel_ptr->mixer_ptr->soloed_channels = g_slist_prepend(channel_ptr->mixer_ptr->soloed_channels, channel); channel_ptr->midi_out_has_events |= CHANNEL_SOLO; LOG_DEBUG("\"%s\" soloed.\n", channel_ptr->name); } void channel_unsolo( jack_mixer_channel_t channel) { if (g_slist_find(channel_ptr->mixer_ptr->soloed_channels, channel) == NULL) return; channel_ptr->mixer_ptr->soloed_channels = g_slist_remove(channel_ptr->mixer_ptr->soloed_channels, channel); channel_ptr->midi_out_has_events |= CHANNEL_SOLO; LOG_DEBUG("\"%s\" un-soloed.\n", channel_ptr->name); } bool channel_is_soloed( jack_mixer_channel_t channel) { if (g_slist_find(channel_ptr->mixer_ptr->soloed_channels, channel)) return true; return false; } void channel_set_midi_scale( jack_mixer_channel_t channel, jack_mixer_scale_t scale) { channel_ptr->midi_scale = scale; } void channel_set_midi_change_callback( jack_mixer_channel_t channel, void (*midi_change_callback) (void*), void *user_data) { channel_ptr->midi_change_callback = midi_change_callback; channel_ptr->midi_change_callback_data = user_data; } bool channel_get_midi_in_got_events( jack_mixer_channel_t channel) { bool t = channel_ptr->midi_in_got_events; channel_ptr->midi_in_got_events = false; return t; } #undef channel_ptr /* * Process input channels and mix them into one output channel signal */ static inline void mix_one( struct output_channel *output_mix_channel, GSList *channels_list, /* All input channels */ jack_nframes_t start, /* Index of first sample to process */ jack_nframes_t end) /* Index of sample to stop processing before */ { jack_nframes_t i; GSList *node_ptr; struct channel * channel_ptr; jack_default_audio_sample_t frame_left; jack_default_audio_sample_t frame_right; jack_default_audio_sample_t frame_left_pre; jack_default_audio_sample_t frame_right_pre; struct channel *mix_channel = (struct channel*)output_mix_channel; /* Zero intermediate mix & output buffers */ for (i = start; i < end; i++) { mix_channel->left_buffer_ptr[i] = mix_channel->tmp_mixed_frames_left[i] = 0.0; if (mix_channel->stereo) mix_channel->right_buffer_ptr[i] = mix_channel->tmp_mixed_frames_right[i] = 0.0; } /* For each input channel: */ for (node_ptr = channels_list; node_ptr; node_ptr = g_slist_next(node_ptr)) { channel_ptr = node_ptr->data; /* Skip input channels with activated mute for this output channel */ if (g_slist_find(output_mix_channel->muted_channels, channel_ptr) != NULL || channel_ptr->out_mute) { continue; } /* Mix signal of all input channels going to this output channel: * * Only add the signal from this input channel: * * - if there are no globally soloed channels and no soloed channels for this output-channel; * - or if the input channel is globally soloed and the output channel is not a system * channel (direct out or monitor out); * - or if the input channel is soloed for this output channel. * * */ if ((!channel_ptr->mixer_ptr->soloed_channels && !output_mix_channel->soloed_channels) || (channel_ptr->mixer_ptr->soloed_channels && g_slist_find(channel_ptr->mixer_ptr->soloed_channels, channel_ptr) != NULL && !output_mix_channel->system) || (output_mix_channel->soloed_channels && g_slist_find(output_mix_channel->soloed_channels, channel_ptr) != NULL)) { /* Get either post or pre-fader signal */ for (i = start ; i < end ; i++) { /* Left/mono signal */ if (! output_mix_channel->prefader && g_slist_find(output_mix_channel->prefader_channels, channel_ptr) == NULL) { frame_left = channel_ptr->frames_left[i-start]; } else { /* Output channel is globally set to pre-fader routing or * input channel has pre-fader routing set for this output channel */ frame_left = channel_ptr->prefader_frames_left[i-start]; } if (frame_left == NAN) break; mix_channel->tmp_mixed_frames_left[i] += frame_left; /* Right signal */ if (mix_channel->stereo) { if (! output_mix_channel->prefader && g_slist_find(output_mix_channel->prefader_channels, channel_ptr) == NULL) { frame_right = channel_ptr->frames_right[i-start]; } else { /* Pre-fader routing */ frame_right = channel_ptr->prefader_frames_right[i-start]; } if (frame_right == NAN) break; mix_channel->tmp_mixed_frames_right[i] += frame_right; } } } } /* Apply output channel volume and compute meter signal and peak values */ unsigned int steps = mix_channel->num_volume_transition_steps; for (i = start ; i < end ; i++) { mix_channel->prefader_frames_left[i] = mix_channel->tmp_mixed_frames_left[i]; mix_channel->prefader_frames_right[i] = mix_channel->tmp_mixed_frames_right[i]; /** Apply fader volume if output channel is not set to pre-fader routing */ if (! output_mix_channel->prefader) { float volume = mix_channel->volume; float volume_new = mix_channel->volume_new; float vol = volume; float balance = mix_channel->balance; float balance_new = mix_channel->balance_new; float bal = balance; /* Do interpolation during transition to target volume level */ if (volume != volume_new) { vol = interpolate(volume, volume_new, mix_channel->volume_idx, steps); } /* Do interpolation during transition to target balance */ if (balance != balance_new) { bal = mix_channel->balance_idx * (balance_new - balance) / steps + balance; } float vol_l; float vol_r; /* Calculate left+right gain from volume and balance levels */ if (mix_channel->stereo) { if (bal > 0) { vol_l = vol * (1 - bal); vol_r = vol; } else { vol_l = vol; vol_r = vol * (1 + bal); } } else { vol_l = vol * (1 - bal); vol_r = vol * (1 + bal); } /* Apply gain to output mix */ mix_channel->tmp_mixed_frames_left[i] *= vol_l; mix_channel->tmp_mixed_frames_right[i] *= vol_r; } /* Get peak signal, left/right and combined */ frame_left = fabsf(mix_channel->tmp_mixed_frames_left[i]); frame_left_pre = fabsf(mix_channel->prefader_frames_left[i]); if (mix_channel->peak_left_prefader < frame_left_pre) { mix_channel->peak_left_prefader = frame_left_pre; } if (mix_channel->peak_left_postfader < frame_left) { mix_channel->peak_left_postfader = frame_left; } if (frame_left > mix_channel->abspeak_postfader) { mix_channel->abspeak_postfader = frame_left; } if (frame_left_pre > mix_channel->abspeak_prefader) { mix_channel->abspeak_prefader = frame_left_pre; } /* This seems to duplicate what was already done right above? */ if (mix_channel->stereo) { frame_right = fabsf(mix_channel->tmp_mixed_frames_right[i]); frame_right_pre = fabsf(mix_channel->prefader_frames_right[i]); if (mix_channel->peak_right_prefader < frame_right_pre) { mix_channel->peak_right_prefader = frame_right_pre; } if (mix_channel->peak_right_postfader < frame_right) { mix_channel->peak_right_postfader = frame_right; } if (frame_right > mix_channel->abspeak_postfader) { mix_channel->abspeak_postfader = frame_right; } if (frame_right_pre > mix_channel->abspeak_prefader) { mix_channel->abspeak_prefader = frame_right_pre; } } /* update left/right peak values every so often */ mix_channel->peak_frames++; if (mix_channel->peak_frames >= PEAK_FRAMES_CHUNK) { mix_channel->meter_left_prefader = mix_channel->peak_left_prefader; mix_channel->peak_left_prefader = 0.0; mix_channel->meter_left_postfader = mix_channel->peak_left_postfader; mix_channel->peak_left_postfader = 0.0; if (mix_channel->stereo) { mix_channel->meter_right_prefader = mix_channel->peak_right_prefader; mix_channel->peak_right_prefader = 0.0; mix_channel->meter_right_postfader = mix_channel->peak_right_postfader; mix_channel->peak_right_postfader = 0.0; } mix_channel->peak_frames = 0; } /* Finish off volume interpolation */ mix_channel->volume_idx++; if ((mix_channel->volume != mix_channel->volume_new) && (mix_channel->volume_idx == steps)) { mix_channel->volume = mix_channel->volume_new; mix_channel->volume_idx = 0; } /* Finish off volume interpolation */ mix_channel->balance_idx++; if ((mix_channel->balance != mix_channel->balance_new) && (mix_channel->balance_idx == steps)) { mix_channel->balance = mix_channel->balance_new; mix_channel->balance_idx = 0; } /* Finally, if output channel is not muted, put signal into output buffer */ if (!mix_channel->out_mute) { mix_channel->left_buffer_ptr[i] = mix_channel->tmp_mixed_frames_left[i]; if (mix_channel->stereo) mix_channel->right_buffer_ptr[i] = mix_channel->tmp_mixed_frames_right[i]; } } /* Calculate k-metering for output channel*/ if (mix_channel->mixer_ptr->kmetering) { kmeter_process(&mix_channel->kmeter_left, mix_channel->tmp_mixed_frames_left, start, end); kmeter_process(&mix_channel->kmeter_right, mix_channel->tmp_mixed_frames_right, start, end); kmeter_process(&mix_channel->kmeter_prefader_left, mix_channel->prefader_frames_left, start, end); kmeter_process(&mix_channel->kmeter_prefader_right, mix_channel->prefader_frames_right, start, end); } } static inline void calc_channel_frames( struct channel *channel_ptr, jack_nframes_t start, jack_nframes_t end) { jack_nframes_t i; jack_default_audio_sample_t frame_left = 0.0f; jack_default_audio_sample_t frame_right = 0.0f; jack_default_audio_sample_t frame_left_pre = 0.0f; jack_default_audio_sample_t frame_right_pre = 0.0f; unsigned int steps = channel_ptr->num_volume_transition_steps; for (i = start ; i < end ; i++) { if (i-start >= MAX_BLOCK_SIZE) { fprintf(stderr, "i-start too high: %d - %d\n", i, start); } /* Save pre-fader signal */ channel_ptr->prefader_frames_left[i-start] = channel_ptr->left_buffer_ptr[i]; if (channel_ptr->stereo) channel_ptr->prefader_frames_right[i-start] = channel_ptr->right_buffer_ptr[i]; /* Detect de-normals */ if (!FLOAT_EXISTS(channel_ptr->left_buffer_ptr[i])) { channel_ptr->NaN_detected = true; channel_ptr->frames_left[i-start] = NAN; break; } /* Get current and target channel volume and balance. */ float volume = channel_ptr->volume; float volume_new = channel_ptr->volume_new; float vol = volume; float balance = channel_ptr->balance; float balance_new = channel_ptr->balance_new; float bal = balance; /* During transition do interpolation to target volume level */ if (channel_ptr->volume != channel_ptr->volume_new) { vol = interpolate(volume, volume_new, channel_ptr->volume_idx, steps); } /* During transition do interpolation to target balance */ if (channel_ptr->balance != channel_ptr->balance_new) { bal = channel_ptr->balance_idx * (balance_new - balance) / steps + balance; } /* Calculate left+right gain from volume and balance levels */ float vol_l; float vol_r; if (channel_ptr->stereo) { if (bal > 0) { vol_l = vol * (1 - bal); vol_r = vol; } else { vol_l = vol; vol_r = vol * (1 + bal); } } else { vol_l = vol * (1 - bal); vol_r = vol * (1 + bal); } /* Calculate left channel post-fader sample */ frame_left = channel_ptr->left_buffer_ptr[i] * vol_l; frame_left_pre = channel_ptr->left_buffer_ptr[i]; /* Calculate right channel post-fader sample */ if (channel_ptr->stereo) { if (!FLOAT_EXISTS(channel_ptr->right_buffer_ptr[i])) { channel_ptr->NaN_detected = true; channel_ptr->frames_right[i-start] = NAN; break; } frame_right = channel_ptr->right_buffer_ptr[i] * vol_r; frame_right_pre = channel_ptr->right_buffer_ptr[i]; } else { frame_right = channel_ptr->left_buffer_ptr[i] * vol_r; frame_right_pre = channel_ptr->left_buffer_ptr[i]; } channel_ptr->frames_left[i-start] = frame_left; channel_ptr->frames_right[i-start] = frame_right; /* Calculate left+right peak-level and, if need be, * update abspeak level */ if (channel_ptr->stereo) { frame_left = fabsf(frame_left); frame_right = fabsf(frame_right); frame_left_pre = fabsf(frame_left_pre); frame_right_pre = fabsf(frame_right_pre); if (channel_ptr->peak_left_prefader < frame_left_pre) { channel_ptr->peak_left_prefader = frame_left_pre; } if (channel_ptr->peak_left_postfader < frame_left) { channel_ptr->peak_left_postfader = frame_left; } if (frame_left > channel_ptr->abspeak_postfader) { channel_ptr->abspeak_postfader = frame_left; } if (frame_left_pre > channel_ptr->abspeak_prefader) { channel_ptr->abspeak_prefader = frame_left_pre; } if (channel_ptr->peak_right_prefader < frame_right_pre) { channel_ptr->peak_right_prefader = frame_right_pre; } if (channel_ptr->peak_right_postfader < frame_right) { channel_ptr->peak_right_postfader = frame_right; } if (frame_right > channel_ptr->abspeak_postfader) { channel_ptr->abspeak_postfader = frame_right; } if (frame_right_pre > channel_ptr->abspeak_prefader) { channel_ptr->abspeak_prefader = frame_right_pre; } } else { frame_left = (fabsf(frame_left) + fabsf(frame_right)) / 2; frame_left_pre = fabsf(frame_left_pre); if (channel_ptr->peak_left_prefader < frame_left_pre) { channel_ptr->peak_left_prefader = frame_left_pre; } if (channel_ptr->peak_left_postfader < frame_left) { channel_ptr->peak_left_postfader = frame_left; } if (frame_left > channel_ptr->abspeak_postfader) { channel_ptr->abspeak_postfader = frame_left; } if (frame_left_pre > channel_ptr->abspeak_prefader) { channel_ptr->abspeak_prefader = frame_left_pre; } } /* Update input channel volume meter every so often */ channel_ptr->peak_frames++; if (channel_ptr->peak_frames >= PEAK_FRAMES_CHUNK) { channel_ptr->meter_left_postfader = channel_ptr->peak_left_postfader; channel_ptr->peak_left_postfader = 0.0; channel_ptr->meter_left_prefader = channel_ptr->peak_left_prefader; channel_ptr->peak_left_prefader = 0.0; if (channel_ptr->stereo) { channel_ptr->meter_right_postfader = channel_ptr->peak_right_postfader; channel_ptr->peak_right_postfader = 0.0; channel_ptr->meter_right_prefader = channel_ptr->peak_right_prefader; channel_ptr->peak_right_prefader = 0.0; } channel_ptr->peak_frames = 0; } /* Finish off volume & balance level interpolation */ channel_ptr->volume_idx++; if ((channel_ptr->volume != channel_ptr->volume_new) && (channel_ptr->volume_idx >= steps)) { channel_ptr->volume = channel_ptr->volume_new; channel_ptr->volume_idx = 0; } channel_ptr->balance_idx++; if ((channel_ptr->balance != channel_ptr->balance_new) && (channel_ptr->balance_idx >= steps)) { channel_ptr->balance = channel_ptr->balance_new; channel_ptr->balance_idx = 0; } } /* Calculate k-metering for input channel */ if (channel_ptr->mixer_ptr->kmetering) { kmeter_process(&channel_ptr->kmeter_left, channel_ptr->frames_left, start, end); if (channel_ptr->stereo) { kmeter_process(&channel_ptr->kmeter_right, channel_ptr->frames_right, start, end); } kmeter_process(&channel_ptr->kmeter_prefader_left, channel_ptr->prefader_frames_left, start, end); if (channel_ptr->stereo) kmeter_process(&channel_ptr->kmeter_prefader_right, channel_ptr->prefader_frames_right, start, end); } } static inline void mix( struct jack_mixer * mixer_ptr, jack_nframes_t start, /* Index of first sample to process */ jack_nframes_t end) /* Index of sample to stop processing before */ { GSList *node_ptr; struct output_channel * output_channel_ptr; struct channel *channel_ptr; /* Calculate pre/post-fader output and peak values for each input channel */ for (node_ptr = mixer_ptr->input_channels_list; node_ptr; node_ptr = g_slist_next(node_ptr)) { channel_ptr = (struct channel*)node_ptr->data; calc_channel_frames(channel_ptr, start, end); } /* For all output channels: */ for (node_ptr = mixer_ptr->output_channels_list; node_ptr; node_ptr = g_slist_next(node_ptr)) { output_channel_ptr = node_ptr->data; channel_ptr = (struct channel*)output_channel_ptr; if (output_channel_ptr->system) { /* Don't bother mixing the channels if we are not connected */ if (channel_ptr->stereo) { if (jack_port_connected(channel_ptr->port_left) == 0 && jack_port_connected(channel_ptr->port_right) == 0) continue; } else { if (jack_port_connected(channel_ptr->port_left) == 0) continue; } } /* Mix this output channel */ mix_one(output_channel_ptr, mixer_ptr->input_channels_list, start, end); } } static inline void update_channel_buffers( struct channel * channel_ptr, jack_nframes_t nframes) { channel_ptr->left_buffer_ptr = jack_port_get_buffer(channel_ptr->port_left, nframes); if (channel_ptr->stereo) { channel_ptr->right_buffer_ptr = jack_port_get_buffer(channel_ptr->port_right, nframes); } } static inline void kmeter_calc_hold_fall( int *hold, float *fall, jack_nframes_t nframes, jack_nframes_t sr) { float t; t = (float) nframes / sr; *hold = (int)(0.5 / t + 0.5f); *fall = powf(10.0f, -0.05f * 10.5f * t); } static inline void set_kmeters_peak_params( struct channel *channel_ptr, jack_nframes_t nframes) { int hold; float fall; jack_nframes_t sr = jack_get_sample_rate(channel_ptr->mixer_ptr->jack_client); kmeter_calc_hold_fall(&hold, &fall, nframes, sr); channel_ptr->kmeter_left._hold = hold; channel_ptr->kmeter_right._hold = hold; channel_ptr->kmeter_left._fall = fall; channel_ptr->kmeter_right._fall = fall; } static int jack_buffer_size_cb(jack_nframes_t nframes, void *arg) { struct jack_mixer *mixer_ptr = (struct jack_mixer *) arg; struct channel *channel_ptr; GSList *list_ptr; /* Get input ports buffer pointers */ for (list_ptr = mixer_ptr->input_channels_list; list_ptr; list_ptr = g_slist_next(list_ptr)) { channel_ptr = list_ptr->data; set_kmeters_peak_params(channel_ptr, nframes); } /* Get output ports buffer pointer */ for (list_ptr = mixer_ptr->output_channels_list; list_ptr; list_ptr = g_slist_next(list_ptr)) { channel_ptr = list_ptr->data; set_kmeters_peak_params(channel_ptr, nframes); } return 0; } #define mixer_ptr ((struct jack_mixer *)context) static int process( jack_nframes_t nframes, void * context) { GSList *node_ptr; struct channel * channel_ptr; #if defined(HAVE_JACK_MIDI) jack_nframes_t i; jack_nframes_t event_count; jack_midi_event_t in_event; unsigned char* midi_out_buffer; void * midi_buffer; double volume, balance; uint8_t cc_channel_index; uint8_t cc_num, cc_val, cur_cc_val; #endif /* Get input ports buffer pointers */ for (node_ptr = mixer_ptr->input_channels_list; node_ptr; node_ptr = g_slist_next(node_ptr)) { channel_ptr = node_ptr->data; update_channel_buffers(channel_ptr, nframes); } /* Get output ports buffer pointer */ for (node_ptr = mixer_ptr->output_channels_list; node_ptr; node_ptr = g_slist_next(node_ptr)) { channel_ptr = node_ptr->data; update_channel_buffers(channel_ptr, nframes); } #if defined(HAVE_JACK_MIDI) midi_buffer = jack_port_get_buffer(mixer_ptr->port_midi_in, nframes); event_count = jack_midi_get_event_count(midi_buffer); for (i = 0 ; i < event_count; i++) { jack_midi_event_get(&in_event, midi_buffer, i); if (in_event.size != 3 || (in_event.buffer[0] & 0xF0) != 0xB0 || in_event.buffer[1] > 127 || in_event.buffer[2] > 127) { continue; } assert(in_event.time < nframes); cc_num = (uint8_t)(in_event.buffer[1] & 0x7F); cc_val = (uint8_t)(in_event.buffer[2] & 0x7F); mixer_ptr->last_midi_cc = (int8_t)cc_num; LOG_DEBUG("%u: CC#%u -> %u\n", (unsigned int)(in_event.buffer[0]), cc_num, cc_val); /* Do we have a mapping for particular CC? */ channel_ptr = mixer_ptr->midi_cc_map[cc_num]; if (channel_ptr) { if (channel_ptr->midi_cc_balance_index == cc_num) { if (cc_val < 63) { balance = MAP(cc_val, 0.0, 63.0, -1.0, -0.015625); } else { balance = MAP(cc_val, 64.0, 127.0, 0.0, 1.0); } if (mixer_ptr->midi_behavior == Pick_Up && !channel_ptr->midi_cc_balance_picked_up && channel_balance_read(channel_ptr) - balance < BALANCE_PICKUP_THRESHOLD) { channel_set_midi_cc_balance_picked_up(channel_ptr, true); } if ((mixer_ptr->midi_behavior == Pick_Up && channel_ptr->midi_cc_balance_picked_up) || mixer_ptr->midi_behavior == Jump_To_Value) { channel_balance_write(channel_ptr, balance); } } else if (channel_ptr->midi_cc_volume_index == cc_num) { /* Is a MIDI scale set for corresponding channel? */ if (channel_ptr->midi_scale) { volume = scale_scale_to_db(channel_ptr->midi_scale, (double)cc_val / 127); if (mixer_ptr->midi_behavior == Pick_Up && !channel_ptr->midi_cc_volume_picked_up) { /* MIDI control in pick-up mode but not picked up yet */ cur_cc_val = (uint8_t)(127 * scale_db_to_scale( channel_ptr->midi_scale, value_to_db(channel_ptr->volume))); if (cc_val == cur_cc_val) { /* Incoming MIDI CC value matches current volume level * --> MIDI control is picked up */ channel_set_midi_cc_volume_picked_up(channel_ptr, true); } } if ((mixer_ptr->midi_behavior == Pick_Up && channel_ptr->midi_cc_volume_picked_up) || mixer_ptr->midi_behavior == Jump_To_Value) { channel_volume_write(channel_ptr, volume); } } } else if (channel_ptr->midi_cc_mute_index == cc_num) { if (cc_val >= 64) { channel_out_mute(channel_ptr); } else { channel_out_unmute(channel_ptr); } } else if (channel_ptr->midi_cc_solo_index == cc_num) { if (cc_val >= 64) { channel_solo(channel_ptr); } else { channel_unsolo(channel_ptr); } } channel_ptr->midi_in_got_events = true; if (channel_ptr->midi_change_callback) { channel_ptr->midi_change_callback(channel_ptr->midi_change_callback_data); } } } midi_buffer = jack_port_get_buffer(mixer_ptr->port_midi_out, nframes); jack_midi_clear_buffer(midi_buffer); for(i=0; imidi_cc_map[cc_channel_index]; if (!channel_ptr) { continue; } if (!channel_ptr->midi_out_has_events) { continue; } if (channel_ptr->midi_out_has_events & CHANNEL_VOLUME && channel_ptr->midi_scale) { midi_out_buffer = jack_midi_event_reserve(midi_buffer, 0, 3); if (!midi_out_buffer) continue; midi_out_buffer[0] = 0xB0; /* control change */ midi_out_buffer[1] = channel_ptr->midi_cc_volume_index; midi_out_buffer[2] = (unsigned char)(127 * scale_db_to_scale(channel_ptr->midi_scale, value_to_db(channel_ptr->volume_new))); LOG_DEBUG( "%u: CC#%u <- %u\n", (unsigned int)midi_out_buffer[0], (unsigned int)midi_out_buffer[1], (unsigned int)midi_out_buffer[2]); } if (channel_ptr->midi_out_has_events & CHANNEL_BALANCE) { midi_out_buffer = jack_midi_event_reserve(midi_buffer, 0, 3); if (!midi_out_buffer) continue; midi_out_buffer[0] = 0xB0; /* control change */ midi_out_buffer[1] = channel_ptr->midi_cc_balance_index; balance = channel_balance_read(channel_ptr); if (balance < 0.0) { midi_out_buffer[2] = (unsigned char)(MAP(balance, -1.0, -0.015625, 0.0, 63.0) + 0.5); } else { midi_out_buffer[2] = (unsigned char)(MAP(balance, 0.0, 1.0, 64.0, 127.0) + 0.5); } LOG_DEBUG( "%u: CC#%u <- %u\n", (unsigned int)midi_out_buffer[0], (unsigned int)midi_out_buffer[1], (unsigned int)midi_out_buffer[2]); } if (channel_ptr->midi_out_has_events & CHANNEL_MUTE) { midi_out_buffer = jack_midi_event_reserve(midi_buffer, 0, 3); if (!midi_out_buffer) continue; midi_out_buffer[0] = 0xB0; /* control change */ midi_out_buffer[1] = channel_ptr->midi_cc_mute_index; midi_out_buffer[2] = (unsigned char)(channel_is_out_muted(channel_ptr) ? 127 : 0); LOG_DEBUG( "%u: CC#%u <- %u\n", (unsigned int)midi_out_buffer[0], (unsigned int)midi_out_buffer[1], (unsigned int)midi_out_buffer[2]); } if (channel_ptr->midi_out_has_events & CHANNEL_SOLO) { midi_out_buffer = jack_midi_event_reserve(midi_buffer, 0, 3); if (!midi_out_buffer) continue; midi_out_buffer[0] = 0xB0; /* control change */ midi_out_buffer[1] = channel_ptr->midi_cc_solo_index; midi_out_buffer[2] = (unsigned char)(channel_is_soloed(channel_ptr) ? 127 : 0); LOG_DEBUG( "%u: CC#%u <- %u\n", (unsigned int)midi_out_buffer[0], (unsigned int)midi_out_buffer[1], (unsigned int)midi_out_buffer[2]); } channel_ptr->midi_out_has_events = 0; } } #endif mix(mixer_ptr, 0, nframes); return 0; } #undef mixer_ptr jack_mixer_t create( const char * jack_client_name_ptr, bool stereo) { (void) stereo; int ret; struct jack_mixer * mixer_ptr; int i; char * localedir; localedir = getenv("LOCALEDIR"); setlocale(LC_ALL, ""); bindtextdomain("jack_mixer", localedir != NULL ? localedir : LOCALEDIR); textdomain("jack_mixer"); mixer_ptr = malloc(sizeof(struct jack_mixer)); if (mixer_ptr == NULL) { goto exit; } ret = pthread_mutex_init(&mixer_ptr->mutex, NULL); if (ret != 0) { goto exit_free; } mixer_ptr->input_channels_list = NULL; mixer_ptr->output_channels_list = NULL; mixer_ptr->soloed_channels = NULL; mixer_ptr->kmetering = true; mixer_ptr->last_midi_cc = -1; mixer_ptr->midi_behavior = Jump_To_Value; for (i = 0 ; i < 128 ; i++) { mixer_ptr->midi_cc_map[i] = NULL; } LOG_DEBUG("Initializing JACK.\n"); mixer_ptr->jack_client = jack_client_open(jack_client_name_ptr, 0, NULL); if (mixer_ptr->jack_client == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_JACK_CLIENT_CREATE; goto exit_destroy_mutex; } LOG_DEBUG("JACK client created.\n"); LOG_DEBUG("Sample rate: %u\n", jack_get_sample_rate(mixer_ptr->jack_client)); #if defined(HAVE_JACK_MIDI) mixer_ptr->port_midi_in = jack_port_register(mixer_ptr->jack_client, "midi in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0); if (mixer_ptr->port_midi_in == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_JACK_MIDI_IN_CREATE; goto close_jack; } mixer_ptr->port_midi_out = jack_port_register(mixer_ptr->jack_client, "midi out", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0); if (mixer_ptr->port_midi_out == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_JACK_MIDI_OUT_CREATE; goto close_jack; } #endif ret = jack_set_process_callback(mixer_ptr->jack_client, process, mixer_ptr); if (ret != 0) { _jack_mixer_error = JACK_MIXER_ERROR_JACK_SET_PROCESS_CALLBACK; goto close_jack; } ret = jack_set_buffer_size_callback(mixer_ptr->jack_client, jack_buffer_size_cb, mixer_ptr); if (ret != 0) { _jack_mixer_error = JACK_MIXER_ERROR_JACK_SET_BUFFER_SIZE_CALLBACK; goto close_jack; } ret = jack_activate(mixer_ptr->jack_client); if (ret != 0) { _jack_mixer_error = JACK_MIXER_ERROR_JACK_ACTIVATE; goto close_jack; } return mixer_ptr; close_jack: /* this should clear all other resources we obtained through the client handle */ jack_client_close(mixer_ptr->jack_client); exit_destroy_mutex: pthread_mutex_destroy(&mixer_ptr->mutex); exit_free: free(mixer_ptr); exit: return NULL; } #define mixer_ctx_ptr ((struct jack_mixer *)mixer) void destroy( jack_mixer_t mixer) { LOG_DEBUG("Uninitializing JACK.\n"); assert(mixer_ctx_ptr->jack_client != NULL); jack_client_close(mixer_ctx_ptr->jack_client); pthread_mutex_destroy(&mixer_ctx_ptr->mutex); free(mixer_ctx_ptr); } unsigned int get_channels_count( jack_mixer_t mixer) { return g_slist_length(mixer_ctx_ptr->input_channels_list); } const char* get_client_name( jack_mixer_t mixer) { return jack_get_client_name(mixer_ctx_ptr->jack_client); } bool get_kmetering( jack_mixer_t mixer) { return mixer_ctx_ptr->kmetering; } void set_kmetering( jack_mixer_t mixer, bool flag) { mixer_ctx_ptr->kmetering = flag; } int8_t get_last_midi_cc( jack_mixer_t mixer) { return mixer_ctx_ptr->last_midi_cc; } void set_last_midi_cc( jack_mixer_t mixer, int8_t new_cc) { mixer_ctx_ptr->last_midi_cc = new_cc; } int get_midi_behavior_mode( jack_mixer_t mixer) { return mixer_ctx_ptr->midi_behavior; } void set_midi_behavior_mode( jack_mixer_t mixer, enum midi_behavior_mode mode) { mixer_ctx_ptr->midi_behavior = mode; } jack_mixer_channel_t add_channel( jack_mixer_t mixer, const char * channel_name, bool stereo) { struct channel * channel_ptr; char * port_name = NULL; size_t channel_name_size; channel_ptr = malloc(sizeof(struct channel)); if (channel_ptr == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_CHANNEL_MALLOC; goto fail; } channel_ptr->mixer_ptr = mixer_ctx_ptr; channel_ptr->name = strdup(channel_name); if (channel_ptr->name == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_CHANNEL_NAME_MALLOC; goto fail_free_channel; } channel_name_size = strlen(channel_name); if (stereo) { port_name = malloc(channel_name_size + 3); if (port_name == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_CHANNEL_NAME_MALLOC; goto fail_free_channel_name; } memcpy(port_name, channel_name, channel_name_size); port_name[channel_name_size] = ' '; port_name[channel_name_size+1] = 'L'; port_name[channel_name_size+2] = 0; channel_ptr->port_left = jack_port_register(channel_ptr->mixer_ptr->jack_client, port_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); if (channel_ptr->port_left == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_PORT_REGISTER_LEFT; goto fail_free_port_name; } port_name[channel_name_size+1] = 'R'; channel_ptr->port_right = jack_port_register(channel_ptr->mixer_ptr->jack_client, port_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); if (channel_ptr->port_right == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_PORT_REGISTER_RIGHT; goto fail_unregister_left_channel; } } else { channel_ptr->port_left = jack_port_register(channel_ptr->mixer_ptr->jack_client, channel_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); if (channel_ptr->port_left == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_PORT_REGISTER; goto fail_free_channel_name; } } channel_ptr->stereo = stereo; int sr = jack_get_sample_rate(channel_ptr->mixer_ptr->jack_client); int fsize = jack_get_buffer_size(channel_ptr->mixer_ptr->jack_client); channel_ptr->volume_transition_seconds = VOLUME_TRANSITION_SECONDS; channel_ptr->num_volume_transition_steps = channel_ptr->volume_transition_seconds * sr + 1; channel_ptr->volume = 0.0; channel_ptr->volume_new = 0.0; channel_ptr->balance = 0.0; channel_ptr->balance_new = 0.0; channel_ptr->meter_left_prefader = channel_ptr->meter_left_postfader = -1.0; channel_ptr->meter_right_prefader = channel_ptr->meter_right_postfader = -1.0; channel_ptr->abspeak_postfader = 0.0; channel_ptr->abspeak_prefader = 0.0; channel_ptr->out_mute = false; kmeter_init(&channel_ptr->kmeter_left, fsize, sr); kmeter_init(&channel_ptr->kmeter_right, fsize, sr); kmeter_init(&channel_ptr->kmeter_prefader_left, fsize, sr); kmeter_init(&channel_ptr->kmeter_prefader_right, fsize, sr); channel_ptr->peak_left_prefader = channel_ptr->peak_left_postfader = 0.0; channel_ptr->peak_right_prefader = channel_ptr->peak_right_postfader = 0.0; channel_ptr->peak_frames = 0; channel_ptr->frames_left = calloc(MAX_BLOCK_SIZE, sizeof(jack_default_audio_sample_t)); channel_ptr->frames_right = calloc(MAX_BLOCK_SIZE, sizeof(jack_default_audio_sample_t)); channel_ptr->prefader_frames_left = calloc(MAX_BLOCK_SIZE, sizeof(jack_default_audio_sample_t)); channel_ptr->prefader_frames_right = calloc(MAX_BLOCK_SIZE, sizeof(jack_default_audio_sample_t)); channel_ptr->NaN_detected = false; channel_ptr->midi_cc_volume_index = -1; channel_ptr->midi_cc_balance_index = -1; channel_ptr->midi_cc_mute_index = -1; channel_ptr->midi_cc_solo_index = -1; channel_ptr->midi_cc_volume_picked_up = false; channel_ptr->midi_cc_balance_picked_up = false; channel_ptr->midi_change_callback = NULL; channel_ptr->midi_change_callback_data = NULL; channel_ptr->midi_out_has_events = 0; channel_ptr->midi_scale = NULL; channel_ptr->mixer_ptr->input_channels_list = g_slist_prepend( channel_ptr->mixer_ptr->input_channels_list, channel_ptr); free(port_name); return channel_ptr; fail_unregister_left_channel: jack_port_unregister(channel_ptr->mixer_ptr->jack_client, channel_ptr->port_left); fail_free_port_name: free(port_name); fail_free_channel_name: free(channel_ptr->name); fail_free_channel: free(channel_ptr); channel_ptr = NULL; fail: return NULL; } static jack_mixer_output_channel_t create_output_channel( jack_mixer_t mixer, const char * channel_name, bool stereo, bool system) { struct channel * channel_ptr; struct output_channel * output_channel_ptr; char * port_name = NULL; size_t channel_name_size; output_channel_ptr = malloc(sizeof(struct output_channel)); channel_ptr = (struct channel*)output_channel_ptr; if (channel_ptr == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_CHANNEL_MALLOC; goto fail; } channel_ptr->mixer_ptr = mixer_ctx_ptr; channel_ptr->name = strdup(channel_name); if (channel_ptr->name == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_CHANNEL_NAME_MALLOC; goto fail_free_channel; } if (stereo) { channel_name_size = strlen(channel_name); port_name = malloc(channel_name_size + 4); if (port_name == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_CHANNEL_NAME_MALLOC; goto fail_free_channel_name; } memcpy(port_name, channel_name, channel_name_size); port_name[channel_name_size] = ' '; port_name[channel_name_size+1] = 'L'; port_name[channel_name_size+2] = 0; channel_ptr->port_left = jack_port_register(channel_ptr->mixer_ptr->jack_client, port_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); if (channel_ptr->port_left == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_PORT_REGISTER_LEFT; goto fail_free_port_name; } port_name[channel_name_size+1] = 'R'; channel_ptr->port_right = jack_port_register(channel_ptr->mixer_ptr->jack_client, port_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); if (channel_ptr->port_right == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_PORT_REGISTER_RIGHT; goto fail_unregister_left_channel; } } else { channel_ptr->port_left = jack_port_register(channel_ptr->mixer_ptr->jack_client, channel_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); if (channel_ptr->port_left == NULL) { _jack_mixer_error = JACK_MIXER_ERROR_PORT_REGISTER; goto fail_free_channel_name; } } channel_ptr->stereo = stereo; channel_ptr->out_mute = false; int sr = jack_get_sample_rate(channel_ptr->mixer_ptr->jack_client); int fsize = jack_get_buffer_size(channel_ptr->mixer_ptr->jack_client); channel_ptr->volume_transition_seconds = VOLUME_TRANSITION_SECONDS; channel_ptr->num_volume_transition_steps = channel_ptr->volume_transition_seconds * sr + 1; channel_ptr->volume = 0.0; channel_ptr->volume_new = 0.0; channel_ptr->balance = 0.0; channel_ptr->balance_new = 0.0; channel_ptr->meter_left_prefader = channel_ptr->meter_left_postfader = -1.0; channel_ptr->meter_right_prefader = channel_ptr->meter_right_postfader = -1.0; channel_ptr->abspeak_postfader = 0.0; channel_ptr->abspeak_prefader = 0.0; kmeter_init(&channel_ptr->kmeter_left, fsize, sr); kmeter_init(&channel_ptr->kmeter_right, fsize, sr); kmeter_init(&channel_ptr->kmeter_prefader_left, fsize, sr); kmeter_init(&channel_ptr->kmeter_prefader_right, fsize, sr); channel_ptr->peak_left_prefader = channel_ptr->peak_left_postfader = 0.0; channel_ptr->peak_right_prefader = channel_ptr->peak_right_postfader = 0.0; channel_ptr->peak_frames = 0; channel_ptr->tmp_mixed_frames_left = calloc(MAX_BLOCK_SIZE, sizeof(jack_default_audio_sample_t)); channel_ptr->tmp_mixed_frames_right = calloc(MAX_BLOCK_SIZE, sizeof(jack_default_audio_sample_t)); channel_ptr->frames_left = calloc(MAX_BLOCK_SIZE, sizeof(jack_default_audio_sample_t)); channel_ptr->frames_right = calloc(MAX_BLOCK_SIZE, sizeof(jack_default_audio_sample_t)); channel_ptr->prefader_frames_left = calloc(MAX_BLOCK_SIZE, sizeof(jack_default_audio_sample_t)); channel_ptr->prefader_frames_right = calloc(MAX_BLOCK_SIZE, sizeof(jack_default_audio_sample_t)); channel_ptr->NaN_detected = false; channel_ptr->midi_cc_volume_index = -1; channel_ptr->midi_cc_balance_index = -1; channel_ptr->midi_cc_mute_index = -1; channel_ptr->midi_cc_solo_index = -1; channel_ptr->midi_cc_volume_picked_up = false; channel_ptr->midi_cc_balance_picked_up = false; channel_ptr->midi_change_callback = NULL; channel_ptr->midi_change_callback_data = NULL; channel_ptr->midi_scale = NULL; output_channel_ptr->soloed_channels = NULL; output_channel_ptr->muted_channels = NULL; output_channel_ptr->prefader_channels = NULL; output_channel_ptr->system = system; output_channel_ptr->prefader = false; free(port_name); return output_channel_ptr; fail_unregister_left_channel: jack_port_unregister(channel_ptr->mixer_ptr->jack_client, channel_ptr->port_left); fail_free_port_name: free(port_name); fail_free_channel_name: free(channel_ptr->name); fail_free_channel: free(channel_ptr); channel_ptr = NULL; fail: return NULL; } jack_mixer_output_channel_t add_output_channel( jack_mixer_t mixer, const char * channel_name, bool stereo, bool system) { struct output_channel *output_channel_ptr; struct channel *channel_ptr; output_channel_ptr = create_output_channel(mixer, channel_name, stereo, system); if (output_channel_ptr == NULL) { return NULL; } channel_ptr = (struct channel*)output_channel_ptr; ((struct jack_mixer*)mixer)->output_channels_list = g_slist_prepend( ((struct jack_mixer*)mixer)->output_channels_list, channel_ptr); return output_channel_ptr; } void remove_channels( jack_mixer_t mixer) { GSList *list_ptr; for (list_ptr = mixer_ctx_ptr->input_channels_list; list_ptr; list_ptr = g_slist_next(list_ptr)) { struct channel *input_channel_ptr = list_ptr->data; remove_channel((jack_mixer_channel_t)input_channel_ptr); } } #define km ((struct kmeter *) kmeter) void kmeter_init( jack_mixer_kmeter_t kmeter, jack_nframes_t fsize, jack_nframes_t sr) { km->_z1 = 0; km->_z2 = 0; km->_rms = 0; km->_dpk = 0; km->_cnt = 0; km->_flag = false; km->_omega = 9.72f / sr; kmeter_calc_hold_fall(&km->_hold, &km->_fall, fsize, sr); } void kmeter_process( jack_mixer_kmeter_t kmeter, jack_default_audio_sample_t *p, int start, int end) { int i; jack_default_audio_sample_t s, t, z1, z2; if (km->_flag) { km->_rms = 0; km->_flag = 0; } z1 = km->_z1; z2 = km->_z2; t = 0; for (i = start; i < end; i++) { s = p[i]; s *= s; if (t < s) t = s; z1 += km->_omega * (s - z1); z2 += km->_omega * (z1 - z2); } t = sqrtf(t); km->_z1 = z1 + 1e-20f; km->_z2 = z2 + 1e-20f; s = sqrtf(2 * z2); if (s > km->_rms) km->_rms = s; if (t > km->_dpk) { km->_dpk = t; km->_cnt = km->_hold; } else if (km->_cnt) { km->_cnt--; } else { km->_dpk *= km->_fall; km->_dpk += 1e-10f; } } void remove_output_channel( jack_mixer_output_channel_t output_channel) { struct output_channel *output_channel_ptr = output_channel; struct channel *channel_ptr = output_channel; channel_ptr->mixer_ptr->output_channels_list = g_slist_remove( channel_ptr->mixer_ptr->output_channels_list, channel_ptr); free(channel_ptr->name); jack_port_unregister(channel_ptr->mixer_ptr->jack_client, channel_ptr->port_left); if (channel_ptr->stereo) { jack_port_unregister(channel_ptr->mixer_ptr->jack_client, channel_ptr->port_right); } if (channel_ptr->midi_cc_volume_index != -1) { assert(channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_volume_index] == channel_ptr); channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_volume_index] = NULL; } if (channel_ptr->midi_cc_balance_index != -1) { assert(channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_balance_index] == channel_ptr); channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_balance_index] = NULL; } if (channel_ptr->midi_cc_mute_index != -1) { assert(channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_mute_index] == channel_ptr); channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_mute_index] = NULL; } if (channel_ptr->midi_cc_solo_index != -1) { assert(channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_solo_index] == channel_ptr); channel_ptr->mixer_ptr->midi_cc_map[channel_ptr->midi_cc_solo_index] = NULL; } g_slist_free(output_channel_ptr->soloed_channels); g_slist_free(output_channel_ptr->muted_channels); g_slist_free(output_channel_ptr->prefader_channels); free(channel_ptr->tmp_mixed_frames_left); free(channel_ptr->tmp_mixed_frames_right); free(channel_ptr->frames_left); free(channel_ptr->frames_right); free(channel_ptr->prefader_frames_left); free(channel_ptr->prefader_frames_right); free(channel_ptr); } void output_channel_set_solo( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel, bool solo_value) { struct output_channel *output_channel_ptr = output_channel; if (solo_value) { if (g_slist_find(output_channel_ptr->soloed_channels, channel) != NULL) return; output_channel_ptr->soloed_channels = g_slist_prepend(output_channel_ptr->soloed_channels, channel); } else { if (g_slist_find(output_channel_ptr->soloed_channels, channel) == NULL) return; output_channel_ptr->soloed_channels = g_slist_remove(output_channel_ptr->soloed_channels, channel); } } void output_channel_set_muted( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel, bool muted_value) { struct output_channel *output_channel_ptr = output_channel; if (muted_value) { if (g_slist_find(output_channel_ptr->muted_channels, channel) != NULL) return; output_channel_ptr->muted_channels = g_slist_prepend(output_channel_ptr->muted_channels, channel); } else { if (g_slist_find(output_channel_ptr->muted_channels, channel) == NULL) return; output_channel_ptr->muted_channels = g_slist_remove(output_channel_ptr->muted_channels, channel); } } bool output_channel_is_muted( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel) { struct output_channel *output_channel_ptr = output_channel; if (g_slist_find(output_channel_ptr->muted_channels, channel) != NULL) return true; return false; } bool output_channel_is_solo( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel) { struct output_channel *output_channel_ptr = output_channel; if (g_slist_find(output_channel_ptr->soloed_channels, channel) != NULL) return true; return false; } void output_channel_set_prefader( jack_mixer_output_channel_t output_channel, bool pfl_value) { struct output_channel *output_channel_ptr = output_channel; output_channel_ptr->prefader = pfl_value; } bool output_channel_is_prefader( jack_mixer_output_channel_t output_channel) { struct output_channel *output_channel_ptr = output_channel; return output_channel_ptr->prefader; } void output_channel_set_in_prefader( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel, bool prefader_value) { struct output_channel *output_channel_ptr = output_channel; if (prefader_value) { if (g_slist_find(output_channel_ptr->prefader_channels, channel) != NULL) return; output_channel_ptr->prefader_channels = g_slist_prepend(output_channel_ptr->prefader_channels, channel); } else { if (g_slist_find(output_channel_ptr->prefader_channels, channel) == NULL) return; output_channel_ptr->prefader_channels = g_slist_remove(output_channel_ptr->prefader_channels, channel); } } bool output_channel_is_in_prefader( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel) { struct output_channel *output_channel_ptr = output_channel; if (g_slist_find(output_channel_ptr->prefader_channels, channel) != NULL) return true; return false; } jack_mixer-release-17/src/jack_mixer.h000066400000000000000000000204021413224161500201060ustar00rootroot00000000000000/* -*- Mode: C ; c-basic-offset: 2 -*- */ /***************************************************************************** * * This file is part of jack_mixer * * Copyright (C) 2006 Nedko Arnaudov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * *****************************************************************************/ #ifndef _JACK_MIXER_H #define _JACK_MIXER_H #include #include #include #include "scale.h" typedef void * jack_mixer_t; typedef void * jack_mixer_channel_t; typedef void * jack_mixer_kmeter_t; typedef void * jack_mixer_output_channel_t; typedef void * jack_mixer_threshold_t; /* Masks bits for channel.midi_out_has_events */ #define CHANNEL_VOLUME 1 #define CHANNEL_BALANCE 2 #define CHANNEL_MUTE 4 #define CHANNEL_SOLO 8 #define VOLUME_TRANSITION_SECONDS 0.01 #define BALANCE_PICKUP_THRESHOLD 0.015625 // -1.0..+1.0 / 128 #define PEAK_FRAMES_CHUNK 4800 // we don't know how much to allocate, but we don't want to wait with // allocating until we're in the process() callback, so we just take a // fairly big chunk: 4 periods per buffer, 4096 samples per period. // (not sure if the '*4' is needed) #define MAX_BLOCK_SIZE (4 * 4096) #define FLOAT_EXISTS(x) (!((x) - (x))) #ifndef MAP #define MAP(v, imin, imax, omin, omax) (((v) - (imin)) * ((omax) - (omin)) / ((imax) - (imin)) + (omin)) #endif enum midi_behavior_mode { Jump_To_Value, Pick_Up }; enum meter_mode { Pre_Fader, Post_Fader }; typedef enum { JACK_MIXER_NO_ERROR, JACK_MIXER_ERROR_JACK_CLIENT_CREATE, JACK_MIXER_ERROR_JACK_MIDI_IN_CREATE, JACK_MIXER_ERROR_JACK_MIDI_OUT_CREATE, JACK_MIXER_ERROR_JACK_SET_PROCESS_CALLBACK, JACK_MIXER_ERROR_JACK_SET_BUFFER_SIZE_CALLBACK, JACK_MIXER_ERROR_JACK_ACTIVATE, JACK_MIXER_ERROR_CHANNEL_MALLOC, JACK_MIXER_ERROR_CHANNEL_NAME_MALLOC, JACK_MIXER_ERROR_PORT_REGISTER, JACK_MIXER_ERROR_PORT_REGISTER_LEFT, JACK_MIXER_ERROR_PORT_REGISTER_RIGHT, JACK_MIXER_ERROR_JACK_RENAME_PORT, JACK_MIXER_ERROR_JACK_RENAME_PORT_LEFT, JACK_MIXER_ERROR_JACK_RENAME_PORT_RIGHT, JACK_MIXER_ERROR_PORT_NAME_MALLOC, JACK_MIXER_ERROR_INVALID_CC, JACK_MIXER_ERROR_NO_FREE_CC, JACK_MIXER_ERROR_COUNT } jack_mixer_error_t; jack_mixer_error_t jack_mixer_error(); const char* jack_mixer_error_str(); jack_mixer_t create( const char * jack_client_name_ptr, bool stereo); void destroy( jack_mixer_t mixer); unsigned int get_channels_count( jack_mixer_t mixer); const char* get_client_name( jack_mixer_t mixer); bool get_kmetering( jack_mixer_t mixer); void set_kmetering( jack_mixer_t mixer, bool flag); int8_t get_last_midi_cc( jack_mixer_t mixer); void set_last_midi_cc( jack_mixer_t mixer, int8_t new_cc); int get_midi_behavior_mode( jack_mixer_t mixer); void set_midi_behavior_mode( jack_mixer_t mixer, enum midi_behavior_mode mode); jack_mixer_channel_t add_channel( jack_mixer_t mixer, const char * channel_name, bool stereo); void kmeter_init( jack_mixer_kmeter_t km, jack_nframes_t fsize, jack_nframes_t sr ); void kmeter_process( jack_mixer_kmeter_t km, jack_default_audio_sample_t *p, int start, int end); const char * channel_get_name( jack_mixer_channel_t channel); /* returned values are in dBFS */ void channel_stereo_meter_read( jack_mixer_channel_t channel, double * left_ptr, double * right_ptr, enum meter_mode); /* returned value is in dBFS */ void channel_mono_meter_read( jack_mixer_channel_t channel, double * mono_ptr, enum meter_mode); /* returned values are in dBFS */ void channel_stereo_kmeter_read( jack_mixer_channel_t channel, double * left_ptr, double * right_ptr, double * left_rms_ptr, double * right_rms_ptr, enum meter_mode); /* returned value is in dBFS */ void channel_mono_kmeter_read( jack_mixer_channel_t channel, double * mono_ptr, double * mono_rms_ptr, enum meter_mode mode); void channel_mono_kmeter_reset( jack_mixer_channel_t channel); void channel_stereo_kmeter_reset( jack_mixer_channel_t channel); bool channel_is_stereo( jack_mixer_channel_t channel); void channel_set_midi_change_callback( jack_mixer_channel_t channel, void (*midi_change_callback) (void*), void *user_data); /* volume is in dBFS */ void channel_volume_write( jack_mixer_channel_t channel, double volume); double channel_volume_read( jack_mixer_channel_t channel); void channels_volumes_read( jack_mixer_t mixer_ptr); /* balance is from -1.0 (full left) to +1.0 (full right) */ void channel_balance_write( jack_mixer_channel_t channel, double balance); double channel_balance_read( jack_mixer_channel_t channel); int8_t channel_get_balance_midi_cc( jack_mixer_channel_t channel); int channel_set_balance_midi_cc( jack_mixer_channel_t channel, int8_t new_cc); int8_t channel_get_volume_midi_cc( jack_mixer_channel_t channel); int channel_set_volume_midi_cc( jack_mixer_channel_t channel, int8_t new_cc); int8_t channel_get_mute_midi_cc( jack_mixer_channel_t channel); int channel_set_mute_midi_cc( jack_mixer_channel_t channel, int8_t new_cc); int8_t channel_get_solo_midi_cc( jack_mixer_channel_t channel); int channel_set_solo_midi_cc( jack_mixer_channel_t channel, int8_t new_cc); void channel_set_midi_cc_volume_picked_up( jack_mixer_channel_t channel, bool status); void channel_set_midi_cc_balance_picked_up( jack_mixer_channel_t channel, bool status); int channel_autoset_volume_midi_cc( jack_mixer_channel_t channel); int channel_autoset_balance_midi_cc( jack_mixer_channel_t channel); int channel_autoset_mute_midi_cc( jack_mixer_channel_t channel); int channel_autoset_solo_midi_cc( jack_mixer_channel_t channel); void remove_channel( jack_mixer_channel_t channel); void remove_channels( jack_mixer_t mixer); /* returned value is in dBFS */ double channel_abspeak_read( jack_mixer_channel_t channel, enum meter_mode modes); void channel_abspeak_reset( jack_mixer_channel_t channel, enum meter_mode mode); void channel_out_mute( jack_mixer_channel_t channel); void channel_out_unmute( jack_mixer_channel_t channel); bool channel_is_out_muted( jack_mixer_channel_t channel); void channel_solo( jack_mixer_channel_t channel); void channel_unsolo( jack_mixer_channel_t channel); bool channel_is_soloed( jack_mixer_channel_t channel); int channel_rename( jack_mixer_channel_t channel, const char * name); void channel_set_midi_scale( jack_mixer_channel_t channel, jack_mixer_scale_t scale); bool channel_get_midi_in_got_events( jack_mixer_channel_t channel); jack_mixer_output_channel_t add_output_channel( jack_mixer_t mixer, const char * channel_name, bool stereo, bool system); void remove_output_channel( jack_mixer_output_channel_t output_channel); void output_channel_set_solo( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel, bool solo_value); void output_channel_set_muted( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel, bool muted_value); bool output_channel_is_muted( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel); bool output_channel_is_solo( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel); void output_channel_set_prefader( jack_mixer_output_channel_t output_channel, bool pfl_value); bool output_channel_is_prefader( jack_mixer_output_channel_t output_channel); void output_channel_set_in_prefader( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t input_channel, bool prefader_value); bool output_channel_is_in_prefader( jack_mixer_output_channel_t output_channel, jack_mixer_channel_t channel); #endif /* #ifndef _JACK_MIXER_H */ jack_mixer-release-17/src/list.h000066400000000000000000000672101413224161500167550ustar00rootroot00000000000000/* -*- Mode: C ; c-basic-offset: 2 -*- */ /***************************************************************************** * * Linux kernel header adapted for user-mode * The 2.6.17-rt1 version was used. * * Original copyright holders of this code are unknown, they were not * mentioned in the original file. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * *****************************************************************************/ #ifndef _LINUX_LIST_H #define _LINUX_LIST_H #include #if !defined(offsetof) #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) #endif /** * container_of - cast a member of a structure out to the containing structure * @ptr: the pointer to the member. * @type: the type of the container struct this is embedded in. * @member: the name of the member within the struct. * */ #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );}) #define prefetch(x) (x = x) /* * These are non-NULL pointers that will result in page faults * under normal circumstances, used to verify that nobody uses * non-initialized list entries. */ #define LIST_POISON1 ((void *) 0x00100100) #define LIST_POISON2 ((void *) 0x00200200) /* * Simple doubly linked list implementation. * * Some of the internal functions ("__xxx") are useful when * manipulating whole lists rather than single entries, as * sometimes we already know the next/prev entries and we can * generate better code by using them directly rather than * using the generic single-entry routines. */ struct list_head { struct list_head *next, *prev; }; #define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name) static inline void INIT_LIST_HEAD(struct list_head *list) { list->next = list; list->prev = list; } /* * Insert a new entry between two known consecutive entries. * * This is only for internal list manipulation where we know * the prev/next entries already! */ static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { next->prev = new; new->next = next; new->prev = prev; prev->next = new; } /** * list_add - add a new entry * @new: new entry to be added * @head: list head to add it after * * Insert a new entry after the specified head. * This is good for implementing stacks. */ static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } /** * list_add_tail - add a new entry * @new: new entry to be added * @head: list head to add it before * * Insert a new entry before the specified head. * This is useful for implementing queues. */ static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } /* * Insert a new entry between two known consecutive entries. * * This is only for internal list manipulation where we know * the prev/next entries already! */ static inline void __list_add_rcu(struct list_head * new, struct list_head * prev, struct list_head * next) { new->next = next; new->prev = prev; // smp_wmb(); next->prev = new; prev->next = new; } /** * list_add_rcu - add a new entry to rcu-protected list * @new: new entry to be added * @head: list head to add it after * * Insert a new entry after the specified head. * This is good for implementing stacks. * * The caller must take whatever precautions are necessary * (such as holding appropriate locks) to avoid racing * with another list-mutation primitive, such as list_add_rcu() * or list_del_rcu(), running on this same list. * However, it is perfectly legal to run concurrently with * the _rcu list-traversal primitives, such as * list_for_each_entry_rcu(). */ static inline void list_add_rcu(struct list_head *new, struct list_head *head) { __list_add_rcu(new, head, head->next); } /** * list_add_tail_rcu - add a new entry to rcu-protected list * @new: new entry to be added * @head: list head to add it before * * Insert a new entry before the specified head. * This is useful for implementing queues. * * The caller must take whatever precautions are necessary * (such as holding appropriate locks) to avoid racing * with another list-mutation primitive, such as list_add_tail_rcu() * or list_del_rcu(), running on this same list. * However, it is perfectly legal to run concurrently with * the _rcu list-traversal primitives, such as * list_for_each_entry_rcu(). */ static inline void list_add_tail_rcu(struct list_head *new, struct list_head *head) { __list_add_rcu(new, head->prev, head); } /* * Delete a list entry by making the prev/next entries * point to each other. * * This is only for internal list manipulation where we know * the prev/next entries already! */ static inline void __list_del(struct list_head * prev, struct list_head * next) { next->prev = prev; prev->next = next; } /** * list_del - deletes entry from list. * @entry: the element to delete from the list. * Note: list_empty on entry does not return true after this, the entry is * in an undefined state. */ static inline void list_del(struct list_head *entry) { __list_del(entry->prev, entry->next); entry->next = LIST_POISON1; entry->prev = LIST_POISON2; } /** * list_del_rcu - deletes entry from list without re-initialization * @entry: the element to delete from the list. * * Note: list_empty on entry does not return true after this, * the entry is in an undefined state. It is useful for RCU based * lockfree traversal. * * In particular, it means that we can not poison the forward * pointers that may still be used for walking the list. * * The caller must take whatever precautions are necessary * (such as holding appropriate locks) to avoid racing * with another list-mutation primitive, such as list_del_rcu() * or list_add_rcu(), running on this same list. * However, it is perfectly legal to run concurrently with * the _rcu list-traversal primitives, such as * list_for_each_entry_rcu(). * * Note that the caller is not permitted to immediately free * the newly deleted entry. Instead, either synchronize_rcu() * or call_rcu() must be used to defer freeing until an RCU * grace period has elapsed. */ static inline void list_del_rcu(struct list_head *entry) { __list_del(entry->prev, entry->next); entry->prev = LIST_POISON2; } /* * list_replace_rcu - replace old entry by new one * @old : the element to be replaced * @new : the new element to insert * * The old entry will be replaced with the new entry atomically. */ static inline void list_replace_rcu(struct list_head *old, struct list_head *new) { new->next = old->next; new->prev = old->prev; // smp_wmb(); new->next->prev = new; new->prev->next = new; old->prev = LIST_POISON2; } /** * list_del_init - deletes entry from list and reinitialize it. * @entry: the element to delete from the list. */ static inline void list_del_init(struct list_head *entry) { __list_del(entry->prev, entry->next); INIT_LIST_HEAD(entry); } /** * list_move - delete from one list and add as another's head * @list: the entry to move * @head: the head that will precede our entry */ static inline void list_move(struct list_head *list, struct list_head *head) { __list_del(list->prev, list->next); list_add(list, head); } /** * list_move_tail - delete from one list and add as another's tail * @list: the entry to move * @head: the head that will follow our entry */ static inline void list_move_tail(struct list_head *list, struct list_head *head) { __list_del(list->prev, list->next); list_add_tail(list, head); } /** * list_empty - tests whether a list is empty * @head: the list to test. */ static inline int list_empty(const struct list_head *head) { return head->next == head; } /** * list_empty_careful - tests whether a list is * empty _and_ checks that no other CPU might be * in the process of still modifying either member * * NOTE: using list_empty_careful() without synchronization * can only be safe if the only activity that can happen * to the list entry is list_del_init(). Eg. it cannot be used * if another CPU could re-list_add() it. * * @head: the list to test. */ static inline int list_empty_careful(const struct list_head *head) { struct list_head *next = head->next; return (next == head) && (next == head->prev); } static inline void __list_splice(struct list_head *list, struct list_head *head) { struct list_head *first = list->next; struct list_head *last = list->prev; struct list_head *at = head->next; first->prev = head; head->next = first; last->next = at; at->prev = last; } /** * list_splice - join two lists * @list: the new list to add. * @head: the place to add it in the first list. */ static inline void list_splice(struct list_head *list, struct list_head *head) { if (!list_empty(list)) __list_splice(list, head); } /** * list_splice_init - join two lists and reinitialise the emptied list. * @list: the new list to add. * @head: the place to add it in the first list. * * The list at @list is reinitialised */ static inline void list_splice_init(struct list_head *list, struct list_head *head) { if (!list_empty(list)) { __list_splice(list, head); INIT_LIST_HEAD(list); } } /** * list_entry - get the struct for this entry * @ptr: the &struct list_head pointer. * @type: the type of the struct this is embedded in. * @member: the name of the list_struct within the struct. */ #define list_entry(ptr, type, member) \ container_of(ptr, type, member) /** * list_for_each - iterate over a list * @pos: the &struct list_head to use as a loop counter. * @head: the head for your list. */ #define list_for_each(pos, head) \ for (pos = (head)->next; prefetch(pos->next), pos != (head); \ pos = pos->next) /** * __list_for_each - iterate over a list * @pos: the &struct list_head to use as a loop counter. * @head: the head for your list. * * This variant differs from list_for_each() in that it's the * simplest possible list iteration code, no prefetching is done. * Use this for code that knows the list to be very short (empty * or 1 entry) most of the time. */ #define __list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); pos = pos->next) /** * list_for_each_prev - iterate over a list backwards * @pos: the &struct list_head to use as a loop counter. * @head: the head for your list. */ #define list_for_each_prev(pos, head) \ for (pos = (head)->prev; prefetch(pos->prev), pos != (head); \ pos = pos->prev) /** * list_for_each_safe - iterate over a list safe against removal of list entry * @pos: the &struct list_head to use as a loop counter. * @n: another &struct list_head to use as temporary storage * @head: the head for your list. */ #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) /** * list_for_each_entry - iterate over list of given type * @pos: the type * to use as a loop counter. * @head: the head for your list. * @member: the name of the list_struct within the struct. */ #define list_for_each_entry(pos, head, member) \ for (pos = list_entry((head)->next, typeof(*pos), member); \ prefetch(pos->member.next), &pos->member != (head); \ pos = list_entry(pos->member.next, typeof(*pos), member)) /** * list_for_each_entry_reverse - iterate backwards over list of given type. * @pos: the type * to use as a loop counter. * @head: the head for your list. * @member: the name of the list_struct within the struct. */ #define list_for_each_entry_reverse(pos, head, member) \ for (pos = list_entry((head)->prev, typeof(*pos), member); \ prefetch(pos->member.prev), &pos->member != (head); \ pos = list_entry(pos->member.prev, typeof(*pos), member)) /** * list_prepare_entry - prepare a pos entry for use as a start point in * list_for_each_entry_continue * @pos: the type * to use as a start point * @head: the head of the list * @member: the name of the list_struct within the struct. */ #define list_prepare_entry(pos, head, member) \ ((pos) ? : list_entry(head, typeof(*pos), member)) /** * list_for_each_entry_continue - iterate over list of given type * continuing after existing point * @pos: the type * to use as a loop counter. * @head: the head for your list. * @member: the name of the list_struct within the struct. */ #define list_for_each_entry_continue(pos, head, member) \ for (pos = list_entry(pos->member.next, typeof(*pos), member); \ prefetch(pos->member.next), &pos->member != (head); \ pos = list_entry(pos->member.next, typeof(*pos), member)) /** * list_for_each_entry_from - iterate over list of given type * continuing from existing point * @pos: the type * to use as a loop counter. * @head: the head for your list. * @member: the name of the list_struct within the struct. */ #define list_for_each_entry_from(pos, head, member) \ for (; prefetch(pos->member.next), &pos->member != (head); \ pos = list_entry(pos->member.next, typeof(*pos), member)) /** * list_for_each_entry_safe - iterate over list of given type safe against removal of list entry * @pos: the type * to use as a loop counter. * @n: another type * to use as temporary storage * @head: the head for your list. * @member: the name of the list_struct within the struct. */ #define list_for_each_entry_safe(pos, n, head, member) \ for (pos = list_entry((head)->next, typeof(*pos), member), \ n = list_entry(pos->member.next, typeof(*pos), member); \ &pos->member != (head); \ pos = n, n = list_entry(n->member.next, typeof(*n), member)) /** * list_for_each_entry_safe_continue - iterate over list of given type * continuing after existing point safe against removal of list entry * @pos: the type * to use as a loop counter. * @n: another type * to use as temporary storage * @head: the head for your list. * @member: the name of the list_struct within the struct. */ #define list_for_each_entry_safe_continue(pos, n, head, member) \ for (pos = list_entry(pos->member.next, typeof(*pos), member), \ n = list_entry(pos->member.next, typeof(*pos), member); \ &pos->member != (head); \ pos = n, n = list_entry(n->member.next, typeof(*n), member)) /** * list_for_each_entry_safe_from - iterate over list of given type * from existing point safe against removal of list entry * @pos: the type * to use as a loop counter. * @n: another type * to use as temporary storage * @head: the head for your list. * @member: the name of the list_struct within the struct. */ #define list_for_each_entry_safe_from(pos, n, head, member) \ for (n = list_entry(pos->member.next, typeof(*pos), member); \ &pos->member != (head); \ pos = n, n = list_entry(n->member.next, typeof(*n), member)) /** * list_for_each_entry_safe_reverse - iterate backwards over list of given type safe against * removal of list entry * @pos: the type * to use as a loop counter. * @n: another type * to use as temporary storage * @head: the head for your list. * @member: the name of the list_struct within the struct. */ #define list_for_each_entry_safe_reverse(pos, n, head, member) \ for (pos = list_entry((head)->prev, typeof(*pos), member), \ n = list_entry(pos->member.prev, typeof(*pos), member); \ &pos->member != (head); \ pos = n, n = list_entry(n->member.prev, typeof(*n), member)) /** * list_for_each_rcu - iterate over an rcu-protected list * @pos: the &struct list_head to use as a loop counter. * @head: the head for your list. * * This list-traversal primitive may safely run concurrently with * the _rcu list-mutation primitives such as list_add_rcu() * as long as the traversal is guarded by rcu_read_lock(). */ #define list_for_each_rcu(pos, head) \ for (pos = (head)->next; \ prefetch(rcu_dereference(pos)->next), pos != (head); \ pos = pos->next) #define __list_for_each_rcu(pos, head) \ for (pos = (head)->next; \ rcu_dereference(pos) != (head); \ pos = pos->next) /** * list_for_each_safe_rcu - iterate over an rcu-protected list safe * against removal of list entry * @pos: the &struct list_head to use as a loop counter. * @n: another &struct list_head to use as temporary storage * @head: the head for your list. * * This list-traversal primitive may safely run concurrently with * the _rcu list-mutation primitives such as list_add_rcu() * as long as the traversal is guarded by rcu_read_lock(). */ #define list_for_each_safe_rcu(pos, n, head) \ for (pos = (head)->next; \ n = rcu_dereference(pos)->next, pos != (head); \ pos = n) /** * list_for_each_entry_rcu - iterate over rcu list of given type * @pos: the type * to use as a loop counter. * @head: the head for your list. * @member: the name of the list_struct within the struct. * * This list-traversal primitive may safely run concurrently with * the _rcu list-mutation primitives such as list_add_rcu() * as long as the traversal is guarded by rcu_read_lock(). */ #define list_for_each_entry_rcu(pos, head, member) \ for (pos = list_entry((head)->next, typeof(*pos), member); \ prefetch(rcu_dereference(pos)->member.next), \ &pos->member != (head); \ pos = list_entry(pos->member.next, typeof(*pos), member)) /** * list_for_each_continue_rcu - iterate over an rcu-protected list * continuing after existing point. * @pos: the &struct list_head to use as a loop counter. * @head: the head for your list. * * This list-traversal primitive may safely run concurrently with * the _rcu list-mutation primitives such as list_add_rcu() * as long as the traversal is guarded by rcu_read_lock(). */ #define list_for_each_continue_rcu(pos, head) \ for ((pos) = (pos)->next; \ prefetch(rcu_dereference((pos))->next), (pos) != (head); \ (pos) = (pos)->next) /* * Double linked lists with a single pointer list head. * Mostly useful for hash tables where the two pointer list head is * too wasteful. * You lose the ability to access the tail in O(1). */ struct hlist_head { struct hlist_node *first; }; struct hlist_node { struct hlist_node *next, **pprev; }; #define HLIST_HEAD_INIT { .first = NULL } #define HLIST_HEAD(name) struct hlist_head name = { .first = NULL } #define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL) static inline void INIT_HLIST_NODE(struct hlist_node *h) { h->next = NULL; h->pprev = NULL; } static inline int hlist_unhashed(const struct hlist_node *h) { return !h->pprev; } static inline int hlist_empty(const struct hlist_head *h) { return !h->first; } static inline void __hlist_del(struct hlist_node *n) { struct hlist_node *next = n->next; struct hlist_node **pprev = n->pprev; *pprev = next; if (next) next->pprev = pprev; } static inline void hlist_del(struct hlist_node *n) { __hlist_del(n); n->next = LIST_POISON1; n->pprev = LIST_POISON2; } /** * hlist_del_rcu - deletes entry from hash list without re-initialization * @n: the element to delete from the hash list. * * Note: list_unhashed() on entry does not return true after this, * the entry is in an undefined state. It is useful for RCU based * lockfree traversal. * * In particular, it means that we can not poison the forward * pointers that may still be used for walking the hash list. * * The caller must take whatever precautions are necessary * (such as holding appropriate locks) to avoid racing * with another list-mutation primitive, such as hlist_add_head_rcu() * or hlist_del_rcu(), running on this same list. * However, it is perfectly legal to run concurrently with * the _rcu list-traversal primitives, such as * hlist_for_each_entry(). */ static inline void hlist_del_rcu(struct hlist_node *n) { __hlist_del(n); n->pprev = LIST_POISON2; } static inline void hlist_del_init(struct hlist_node *n) { if (!hlist_unhashed(n)) { __hlist_del(n); INIT_HLIST_NODE(n); } } /* * hlist_replace_rcu - replace old entry by new one * @old : the element to be replaced * @new : the new element to insert * * The old entry will be replaced with the new entry atomically. */ static inline void hlist_replace_rcu(struct hlist_node *old, struct hlist_node *new) { struct hlist_node *next = old->next; new->next = next; new->pprev = old->pprev; // smp_wmb(); if (next) new->next->pprev = &new->next; *new->pprev = new; old->pprev = LIST_POISON2; } static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) { struct hlist_node *first = h->first; n->next = first; if (first) first->pprev = &n->next; h->first = n; n->pprev = &h->first; } /** * hlist_add_head_rcu - adds the specified element to the specified hlist, * while permitting racing traversals. * @n: the element to add to the hash list. * @h: the list to add to. * * The caller must take whatever precautions are necessary * (such as holding appropriate locks) to avoid racing * with another list-mutation primitive, such as hlist_add_head_rcu() * or hlist_del_rcu(), running on this same list. * However, it is perfectly legal to run concurrently with * the _rcu list-traversal primitives, such as * hlist_for_each_entry_rcu(), used to prevent memory-consistency * problems on Alpha CPUs. Regardless of the type of CPU, the * list-traversal primitive must be guarded by rcu_read_lock(). */ static inline void hlist_add_head_rcu(struct hlist_node *n, struct hlist_head *h) { struct hlist_node *first = h->first; n->next = first; n->pprev = &h->first; // smp_wmb(); if (first) first->pprev = &n->next; h->first = n; } /* next must be != NULL */ static inline void hlist_add_before(struct hlist_node *n, struct hlist_node *next) { n->pprev = next->pprev; n->next = next; next->pprev = &n->next; *(n->pprev) = n; } static inline void hlist_add_after(struct hlist_node *n, struct hlist_node *next) { next->next = n->next; n->next = next; next->pprev = &n->next; if(next->next) next->next->pprev = &next->next; } /** * hlist_add_before_rcu - adds the specified element to the specified hlist * before the specified node while permitting racing traversals. * @n: the new element to add to the hash list. * @next: the existing element to add the new element before. * * The caller must take whatever precautions are necessary * (such as holding appropriate locks) to avoid racing * with another list-mutation primitive, such as hlist_add_head_rcu() * or hlist_del_rcu(), running on this same list. * However, it is perfectly legal to run concurrently with * the _rcu list-traversal primitives, such as * hlist_for_each_entry_rcu(), used to prevent memory-consistency * problems on Alpha CPUs. */ static inline void hlist_add_before_rcu(struct hlist_node *n, struct hlist_node *next) { n->pprev = next->pprev; n->next = next; // smp_wmb(); next->pprev = &n->next; *(n->pprev) = n; } /** * hlist_add_after_rcu - adds the specified element to the specified hlist * after the specified node while permitting racing traversals. * @prev: the existing element to add the new element after. * @n: the new element to add to the hash list. * * The caller must take whatever precautions are necessary * (such as holding appropriate locks) to avoid racing * with another list-mutation primitive, such as hlist_add_head_rcu() * or hlist_del_rcu(), running on this same list. * However, it is perfectly legal to run concurrently with * the _rcu list-traversal primitives, such as * hlist_for_each_entry_rcu(), used to prevent memory-consistency * problems on Alpha CPUs. */ static inline void hlist_add_after_rcu(struct hlist_node *prev, struct hlist_node *n) { n->next = prev->next; n->pprev = &prev->next; // smp_wmb(); prev->next = n; if (n->next) n->next->pprev = &n->next; } #define hlist_entry(ptr, type, member) container_of(ptr,type,member) #define hlist_for_each(pos, head) \ for (pos = (head)->first; pos && ({ prefetch(pos->next); 1; }); \ pos = pos->next) #define hlist_for_each_safe(pos, n, head) \ for (pos = (head)->first; pos && ({ n = pos->next; 1; }); \ pos = n) /** * hlist_for_each_entry - iterate over list of given type * @tpos: the type * to use as a loop counter. * @pos: the &struct hlist_node to use as a loop counter. * @head: the head for your list. * @member: the name of the hlist_node within the struct. */ #define hlist_for_each_entry(tpos, pos, head, member) \ for (pos = (head)->first; \ pos && ({ prefetch(pos->next); 1;}) && \ ({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \ pos = pos->next) /** * hlist_for_each_entry_continue - iterate over a hlist continuing after existing point * @tpos: the type * to use as a loop counter. * @pos: the &struct hlist_node to use as a loop counter. * @member: the name of the hlist_node within the struct. */ #define hlist_for_each_entry_continue(tpos, pos, member) \ for (pos = (pos)->next; \ pos && ({ prefetch(pos->next); 1;}) && \ ({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \ pos = pos->next) /** * hlist_for_each_entry_from - iterate over a hlist continuing from existing point * @tpos: the type * to use as a loop counter. * @pos: the &struct hlist_node to use as a loop counter. * @member: the name of the hlist_node within the struct. */ #define hlist_for_each_entry_from(tpos, pos, member) \ for (; pos && ({ prefetch(pos->next); 1;}) && \ ({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \ pos = pos->next) /** * hlist_for_each_entry_safe - iterate over list of given type safe against removal of list entry * @tpos: the type * to use as a loop counter. * @pos: the &struct hlist_node to use as a loop counter. * @n: another &struct hlist_node to use as temporary storage * @head: the head for your list. * @member: the name of the hlist_node within the struct. */ #define hlist_for_each_entry_safe(tpos, pos, n, head, member) \ for (pos = (head)->first; \ pos && ({ n = pos->next; 1; }) && \ ({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \ pos = n) /** * hlist_for_each_entry_rcu - iterate over rcu list of given type * @tpos: the type * to use as a loop counter. * @pos: the &struct hlist_node to use as a loop counter. * @head: the head for your list. * @member: the name of the hlist_node within the struct. * * This list-traversal primitive may safely run concurrently with * the _rcu list-mutation primitives such as hlist_add_head_rcu() * as long as the traversal is guarded by rcu_read_lock(). */ #define hlist_for_each_entry_rcu(tpos, pos, head, member) \ for (pos = (head)->first; \ rcu_dereference(pos) && ({ prefetch(pos->next); 1;}) && \ ({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \ pos = pos->next) #endif /* #ifndef _LINUX_LIST_H */ jack_mixer-release-17/src/log.c000066400000000000000000000023271413224161500165540ustar00rootroot00000000000000/* -*- Mode: C ; c-basic-offset: 2 -*- */ /***************************************************************************** * * Copyright (C) 2006,2007 Nedko Arnaudov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * *****************************************************************************/ #include #include #include #include #include "log.h" void jack_mixer_log(int level, const char * format, ...) { (void)level; va_list arglist; va_start(arglist, format); vfprintf(stderr, format, arglist); va_end(arglist); } jack_mixer-release-17/src/log.h000066400000000000000000000051561413224161500165640ustar00rootroot00000000000000/* -*- Mode: C ; c-basic-offset: 2 -*- */ /***************************************************************************** * * Copyright (C) 2006,2007 Nedko Arnaudov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * *****************************************************************************/ #ifndef _LOG_H #define _LOG_H void jack_mixer_log(int level, const char * format, ...); #define LOG_LEVEL_DEBUG 0 #define LOG_LEVEL_INFO 1 #define LOG_LEVEL_WARNING 2 #define LOG_LEVEL_NOTICE 3 #define LOG_LEVEL_ERROR 4 #define LOG_LEVEL_FATAL 5 #define LOG_LEVEL_BLACK_HOLE 6 #if !defined(LOG_LEVEL) #define LOG_LEVEL LOG_LEVEL_WARNING #endif #if LOG_LEVEL <= LOG_LEVEL_DEBUG # define LOG_DEBUG(format, ...) \ jack_mixer_log(LOG_LEVEL_DEBUG, \ format, ## __VA_ARGS__) #else # define LOG_DEBUG(format, ...) #endif #if LOG_LEVEL <= LOG_LEVEL_INFO # define LOG_INFO(format, ...) \ jack_mixer_log(LOG_LEVEL_INFO, \ format, ## __VA_ARGS__) #else # define LOG_INFO(format, ...) #endif #if LOG_LEVEL <= LOG_LEVEL_WARNING # define LOG_WARNING(format, ...) \ jack_mixer_log(LOG_LEVEL_WARNING, \ format, ## __VA_ARGS__) #else # define LOG_WARNING(format, ...) #endif #if LOG_LEVEL <= LOG_LEVEL_NOTICE # define LOG_NOTICE(format, ...) \ jack_mixer_log(LOG_LEVEL_NOTICE, \ format, ## __VA_ARGS__) #else # define LOG_NOTICE(format, ...) #endif #if LOG_LEVEL <= LOG_LEVEL_ERROR # define LOG_ERROR(format, ...) \ jack_mixer_log(LOG_LEVEL_ERROR, \ format, ## __VA_ARGS__) #else # define LOG_ERROR(format, ...) #endif #if LOG_LEVEL <= LOG_LEVEL_FATAL # define LOG_FATAL(format, ...) \ jack_mixer_log(LOG_LEVEL_FATAL, \ format, ## __VA_ARGS__) #else # define LOG_FATAL(format, ...) #endif #endif /* #ifndef _LOG_H */ jack_mixer-release-17/src/meson.build000066400000000000000000000030101413224161500177570ustar00rootroot00000000000000jack_mixer_sources = files([ 'jack_mixer.c', 'log.c', 'scale.c' ]) jack_mixer_inc = include_directories('.') # Build 'jack_mix_box' command line program defines = ['-DLOCALEDIR="@0@"'.format(join_paths(get_option('prefix'), get_option('localedir')))] if get_option('jack-midi').enabled() defines += ['-DHAVE_JACK_MIDI'] endif executable( 'jack_mix_box', ['jack_mix_box.c', jack_mixer_sources], dependencies: [ glib_dep, jack_dep, math_dep ], include_directories: jack_mixer_inc, c_args: defines, install: true, ) # Generate extension module C source from Cython sources if get_option('gui').enabled() fs = import('fs') jack_mixer_mod_pyx = '_jack_mixer.pyx' jack_mixer_mod_c = '_jack_mixer.c' cython = find_program('cython3', 'cython', required: false) if fs.exists(jack_mixer_mod_c) jack_mixer_cython = files(jack_mixer_mod_c) elif cython.found() jack_mixer_mod_pxd = '_jack_mixer.pxd' jack_mixer_cython = custom_target( 'jack_mixer_cython', output: jack_mixer_mod_c, input: jack_mixer_mod_pyx, depend_files: [jack_mixer_mod_pyx, jack_mixer_mod_pxd], command: [cython, '-o', '@OUTPUT@', '@INPUT@'], ) else error('The \'cython\' program was not found but is required.\n' + 'Please install Cython from: https://pypi.org/project/Cython/.') endif meson.add_dist_script('meson_dist_cython.py', jack_mixer_mod_c) endif jack_mixer-release-17/src/meson_dist_cython.py000077500000000000000000000022101413224161500217230ustar00rootroot00000000000000#!/usr/bin/env python import argparse import shutil import sys from os import chdir, environ, getcwd from os.path import join from subprocess import run build_root = environ.get("MESON_BUILD_ROOT") dist_root = environ.get("MESON_DIST_ROOT") source_root = environ.get("MESON_SOURCE_ROOT") ap = argparse.ArgumentParser() ap.add_argument("-v", "--verbose", action="store_true", help="Be more verbose.") ap.add_argument("mod_source", nargs="*", help="Cython module C source target(s) (*.c).") args = ap.parse_args() if args.verbose: print("cwd:", getcwd()) print("build root:", build_root) print("dist root:", dist_root) print("source root:", source_root) print("sys.argv:", sys.argv) for mod in args.mod_source: target = join("src", mod) dst = join(dist_root, "src", mod) print("Updating Cython module C source '{}'...".format(target)) cmd = ["ninja"] if args.verbose: cmd += ["-v"] cmd += [target] chdir(build_root) proc = run(cmd) if proc.returncode != 0: sys.exit("'ninja' returned non-zero ({}) for target '{}'.".format(proc.returncode, target)) shutil.copy(target, dst) jack_mixer-release-17/src/scale.c000066400000000000000000000114121413224161500170550ustar00rootroot00000000000000/* -*- Mode: C ; c-basic-offset: 2 -*- */ /***************************************************************************** * * This file is part of jack_mixer * * Copyright (C) 2006 Nedko Arnaudov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * *****************************************************************************/ #include #include #include #include #include #include "scale.h" //#define LOG_LEVEL LOG_LEVEL_DEBUG #include "log.h" #include "list.h" struct threshold { struct list_head scale_siblings; double db; double scale; double a; double b; }; struct scale { struct list_head thresholds; double max_db; }; jack_mixer_scale_t scale_create() { struct scale * scale_ptr; scale_ptr = malloc(sizeof(struct scale)); if (scale_ptr == NULL) { return NULL; } INIT_LIST_HEAD(&scale_ptr->thresholds); scale_ptr->max_db = -INFINITY; LOG_DEBUG("Scale %p created", scale_ptr); return (jack_mixer_scale_t)scale_ptr; } #define scale_ptr ((struct scale *)scale) void scale_destroy( jack_mixer_scale_t scale) { scale_remove_thresholds(scale); free(scale_ptr); } void scale_remove_thresholds( jack_mixer_scale_t scale) { struct threshold * threshold_ptr; struct threshold * node_ptr; list_for_each_entry_safe(threshold_ptr, node_ptr, &scale_ptr->thresholds, scale_siblings) { list_del(&(threshold_ptr->scale_siblings)); free(threshold_ptr); threshold_ptr = NULL; } } bool scale_add_threshold( jack_mixer_scale_t scale, float db, float scale_value) { struct threshold * threshold_ptr; LOG_DEBUG("Adding threshold (%f dBFS -> %f) to scale %p", db, scale, scale_ptr); threshold_ptr = malloc(sizeof(struct threshold)); LOG_DEBUG("Threshold %p created ", threshold_ptr, db, scale); if (threshold_ptr == NULL) { return false; } threshold_ptr->db = db; threshold_ptr->scale = scale_value; list_add_tail(&threshold_ptr->scale_siblings, &scale_ptr->thresholds); if (db > scale_ptr->max_db) { scale_ptr->max_db = db; } return true; } #undef threshold_ptr void scale_calculate_coefficients( jack_mixer_scale_t scale) { struct threshold * threshold_ptr; struct threshold * prev_ptr; struct list_head * node_ptr; prev_ptr = NULL; list_for_each(node_ptr, &scale_ptr->thresholds) { threshold_ptr = list_entry(node_ptr, struct threshold, scale_siblings); LOG_DEBUG("Calculating coefficients for threshold %p", threshold_ptr); if (prev_ptr != NULL) { threshold_ptr->a = (prev_ptr->scale - threshold_ptr->scale) / (prev_ptr->db - threshold_ptr->db); threshold_ptr->b = threshold_ptr->scale - threshold_ptr->a * threshold_ptr->db; LOG_DEBUG("%.0f dB - %.0f dB: scale = %f * dB + %f", prev_ptr->db, threshold_ptr->db, threshold_ptr->a, threshold_ptr->b); } prev_ptr = threshold_ptr; } } /* Convert dBFS value to number in range 0.0-1.0 */ double scale_db_to_scale( jack_mixer_scale_t scale, double db) { struct threshold * threshold_ptr; struct threshold * prev_ptr; struct list_head * node_ptr; prev_ptr = NULL; list_for_each(node_ptr, &scale_ptr->thresholds) { threshold_ptr = list_entry(node_ptr, struct threshold, scale_siblings); if (db < threshold_ptr->db) { LOG_DEBUG("Match at %f dB treshold", threshold_ptr->db); if (prev_ptr == NULL) { return 0.0; } return threshold_ptr->a * db + threshold_ptr->b; } prev_ptr = threshold_ptr; } return 1.0; } /* Convert number in range 0.0-1.0 to dBFS value */ double scale_scale_to_db( jack_mixer_scale_t scale, double scale_value) { struct threshold * threshold_ptr; struct threshold * prev_ptr; struct list_head * node_ptr; prev_ptr = NULL; list_for_each(node_ptr, &scale_ptr->thresholds) { threshold_ptr = list_entry(node_ptr, struct threshold, scale_siblings); if (scale_value <= threshold_ptr->scale) { if (prev_ptr == NULL) { return -INFINITY; } return (scale_value - threshold_ptr->b) / threshold_ptr->a; } prev_ptr = threshold_ptr; } return scale_ptr->max_db; } jack_mixer-release-17/src/scale.h000066400000000000000000000030071413224161500170630ustar00rootroot00000000000000/* -*- Mode: C ; c-basic-offset: 2 -*- */ /***************************************************************************** * * This file is part of jack_mixer * * Copyright (C) 2006 Nedko Arnaudov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * *****************************************************************************/ #ifndef _SCALE_H #define _SCALE_H typedef void * jack_mixer_scale_t; jack_mixer_scale_t scale_create(); bool scale_add_threshold( jack_mixer_scale_t scale, float db, float scale_value); void scale_remove_thresholds( jack_mixer_scale_t scale); void scale_calculate_coefficients( jack_mixer_scale_t scale); double scale_db_to_scale( jack_mixer_scale_t scale, double db); double scale_scale_to_db( jack_mixer_scale_t scale, double scale_value); void scale_destroy( jack_mixer_scale_t scale); #endif /* #ifndef _SCALE_H */ jack_mixer-release-17/tests/000077500000000000000000000000001413224161500161765ustar00rootroot00000000000000jack_mixer-release-17/tests/test.py000077500000000000000000000023671413224161500175420ustar00rootroot00000000000000#!/usr/bin/env python # # This file is part of jack_mixer # # Copyright (C) 2006 Nedko Arnaudov # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. import jack_mixer_c mixer = jack_mixer_c.Mixer("test") print("Channels count: %u" % mixer.channels_count) channel = mixer.add_channel("Channel 1", True) if channel.is_stereo: channel_type = "Stereo" else: channel_type = "Mono" channel_name = channel.name print('%s channel "%s"' % (channel_type, channel_name)) print("Channel meter read %s" % repr(channel.meter)) print("Channels count: %u" % mixer.channels_count) channel.remove() print("Channels count: %u" % mixer.channels_count) jack_mixer-release-17/tools/000077500000000000000000000000001413224161500161745ustar00rootroot00000000000000jack_mixer-release-17/tools/compile-messages.py000077500000000000000000000010641413224161500220070ustar00rootroot00000000000000#!/usr/bin/env python3 """Compile translated message catalog *.po files to *.mo with msgfmt.""" import glob import os from os.path import basename, join, splitext from subprocess import run LOCALEDIR = join(os.getcwd(), "data", "locale") for po in glob.glob(join(LOCALEDIR, "*.po")): fn = basename(po) domain, lang = splitext(fn)[0].rsplit("-", 1) langdir = join(LOCALEDIR, lang, "LC_MESSAGES") mo = join(langdir, domain + ".mo") print(f"Compiling {fn} to {mo} ...") os.makedirs(langdir, exist_ok=True) run(["msgfmt", "-o", mo, po]) jack_mixer-release-17/tools/extract-messages.sh000077500000000000000000000015111413224161500220100ustar00rootroot00000000000000#!/bin/bash LOCALEDIR="$(pwd)/data/locale" POT="$LOCALEDIR/jack_mixer.pot" VERSION="$(grep -A 5 project meson.build | grep version | cut -d "'" -f 2)" ARGPARSE_PY="$(python -c 'import argparse; print(argparse.__file__)')" echo "Extracting translatable messages from argparse and jack_mixer/*.py to $POT." xgettext \ --package-name=jack_mixer \ "--package-version=$VERSION" \ "--msgid-bugs-address=https://github.com/jack-mixer/jack_mixer/issues" \ --from-code=utf-8 \ -o "$POT" \ -L Python \ jack_mixer/*.py \ "$ARGPARSE_PY" xgettext \ --package-name=jack_mixer \ "--package-version=$VERSION" \ "--msgid-bugs-address=https://github.com/jack-mixer/jack_mixer/issues" \ --from-code=utf-8 \ --join-existing \ -o "$POT" \ -k_ \ -c \ src/jack_mix_box.c \ src/jack_mixer.c jack_mixer-release-17/tools/jack_mixer.sh000077500000000000000000000007651413224161500206570ustar00rootroot00000000000000#!/bin/bash if ! ls -a builddir/jack_mixer/_jack_mixer.*.so >/dev/null 2>&1 ; then echo "'_jack_mixer' extension module not found." echo "This script is meant to be run from a jack_mixer source directory" echo "Make sure that you have built jack_mixer with meson and created" echo "the language translation files with ./tools/compile-messages.py." exit 1 fi export PYTHONPATH=".:./builddir:$PYTHONPATH" export LOCALEDIR="data/locale" exec "${PYTHON:-python3}" -m jack_mixer "$@" jack_mixer-release-17/tools/merge-messages.py000077500000000000000000000010211413224161500214470ustar00rootroot00000000000000#!/usr/bin/env python3 """Merge new/updated messages from jack_mixer.pot into existing translations with msgmerge.""" import glob import os from os.path import basename, join, splitext from subprocess import run LOCALEDIR = join(os.getcwd(), "data", "locale") for po in glob.glob(join(LOCALEDIR, "*.po")): fn = basename(po) domain, lang = splitext(fn)[0].rsplit("-", 1) pot = join(LOCALEDIR, domain + ".pot") print(f"Merging new/updated messages from {pot} into {po} ...") run(["msgmerge", "-U", po, pot])