rgain-1.2/0000775000175000017500000000000012141224140011467 5ustar fkfk00000000000000rgain-1.2/README0000664000175000017500000001617612141222477012375 0ustar fkfk00000000000000rgain 1.2 -- ReplayGain tools and Python library ================================================ This Python package provides modules to read, write and calculate Replay Gain as well as 2 scripts that utilize these modules to do Replay Gain. Replay Gain [1] is a proposed standard (and has been for some time -- but it's widely accepted) that's designed to solve the problem of varying volumes between different audio files. I won't lay it all out for you here, go read it yourself. [1] http://replaygain.org Requirements ============ - Python 2.6 or 2.7 -- http://python.org/ - Mutagen -- http://code.google.com/p/mutagen/ - GStreamer 1.0 -- http://gstreamer.org/ - PyGObject -- https://live.gnome.org/PyGObject - python-docutils for the manpages -- http://docutils.sourceforge.net/ To install these dependencies on Debian or Ubuntu (12.10 or newer):: # apt-get install python python-mutagen python-docutils python-gi \ gir1.2-gstreamer-1.0 libgstreamer1.0-0 gstreamer1.0-plugins-base \ gstreamer1.0-plugins-good Installation ============ Just install it like any other Python package: unpack, then (as root/with **sudo**):: # python setup.py install **replaygain** ============== This is a program like, say, **vorbisgain** or **mp3gain**, the difference being that instead of supporting a mere one format, it supports several: - Ogg Vorbis (or probably anything you can put into an Ogg container) - Flac - WavPack - MP3 Basic usage is simple:: $ replaygain AUDIOFILE1 AUDIOFILE2 ... There are some options; see them by running:: $ replaygain --help **collectiongain** ================== This program is designed to apply Replay Gain to whole music collections, plus the ability to simply add new files, run **collectiongain** and have it replay-gain those files without asking twice. To use it, simply run:: $ collectiongain PATH_TO_MUSIC and re-run it whenever you add new files. Run:: $ collectiongain --help to see possible options. If, however, you want to find out how exactly **collectiongain** works, read on (but be warned: It's long, boring, technical, incomprehensible and awesome). **collectiongain** runs in two phases: The file collecting phase and the actual run. Prior to analyzing any audio data, **collectiongain** gathers all audio files in the directory and determines a so-called album ID for each from the file's tags: - If the file contains a Musicbrainz album ID, that is used. - Otherwise, if the file contains an *album* tag, it is joined with either * an *albumartist* tag, if that exists, * or the *artist* tag * or nothing if neither tag exists. The resulting artist-album combination is the album ID for that file. - If the file doesn't contain a Musicbrainz album ID or an *album* tag, it is presumed to be a single track without album; it will only get track gain, no album gain. Since this step takes a relatively long time, the album IDs are cached between several runs of **collectiongain**. If a file was modified or a new file was added, the album ID will be (re-)calculated for that file only. The program will also cache an educated guess as to whether a file was already processed and had Replay Gain added -- if **collectiongain** thinks so, that file will totally ignored for the actual run. This flag is set whenever the file is processed in the actual run phase (save for dry runs, which you can enable with the **--dry-run** switch) and is cleared whenever a file was changed. You can disable these assumptions with the **--ignore-cache** switch; in that case, the program will actually physically check every file in your collection for Replay Gain data. For the actual run, **collectiongain** will simply look at all files that have survived the cleansing described above; for files that don't contain Replay Gain information, **collectiongain** will calculate it and write it to the files (use the **--force** flag to calculate gain even if the file already has gain data). Here comes the big moment of the album ID: files that have the same album ID are considered to be one album (duh) for the calculation of album gain. If only one file of an album is missing gain information, the whole album will be recalculated to make sure the data is up-to-date. MP3 formats =========== Proper Replay Gain support for MP3 files is a bit of a mess: on the one hand, there is the **mp3gain** application [1] which was relatively widely used (I don't know if it still is) -- it directly modifies the audio data which has the advantage that it works with pretty much any player, but it also means you have to decide ahead of time whether you want track gain or album gain. Besides, it's just not very elegant. On the other hand, there are at least two commonly used ways to store proper Replay Gain information in ID3v2 tags [2]. Now, in general you don't have to worry about this when using this package: by default, **replaygain** and **collectiongain** will read and write Replay Gain information in the two most commonly used formats. However, if for whatever reason you need more control over the MP3 Replay Gain information, you can use the **--mp3-format** option (supported by both programs) to change the behaviour. Possible choices with this switch are: *replaygain.org* (alias: *fb2k*) Replay Gain information is stored in ID3v2 TXXX frames. This format is specified on the replaygain.org website as the recommended format for MP3 files. Notably, this format is also used by the foobar2000 music player for Windows [3]. *legacy* (alias: *ql*) Replay Gain information is stored in ID3v2.4 RVA2 frames. This format is described as "legacy" by replaygain.org; however, it is still the primary format for at least the Quod Libet music player [4] and possibly others. It should be noted that this format does not support volume adjustments of more than 64 dB: if the calculated gain value is smaller than -64 dB or greater than or equal to +64 dB, it is clamped to these limit values. *default* This is the default implementation used by both **replaygain** and **collectiongain**. When writing Replay Gain data, both the *replaygain.org* as well as the *legacy* format are written. As for reading, if a file contains data in both formats, both data sets are read and then compared. If they match up, that Replay Gain information is returned for the file. However, if they don't match, no Replay Gain data is returned to signal that this file does not contain valid (read: consistent) Replay Gain information. [1] http://mp3gain.sourceforce.net [2] http://wiki.hydrogenaudio.org/index.php?title=ReplayGain_specification#ID3v2 [3] http://foobar2000.org [4] http://code.google.com/p/quodlibet Changes ======= rgain 1.2 (2013-05-04) ---------------------- - Port to GStreamer 1.0. - Support default GStreamer command-line options for **replaygain** and **collectiongain**. All known GStreamer options can be listed by using the **--help-gst** flag. Copyright ========= With the exception of the manpages, all files are:: Copyright (c) 2009-2013 Felix Krull The manpages were originally written for the Debian project and are:: Copyright (c) 2011 Simon Chopin Copyright (c) 2012 Felix Krull rgain-1.2/PKG-INFO0000664000175000017500000000260412141224140012566 0ustar fkfk00000000000000Metadata-Version: 1.1 Name: rgain Version: 1.2 Summary: Multi-format Replay Gain utilities Home-page: http://bitbucket.org/fk/rgain Author: Felix Krull Author-email: f_krull@gmx.de License: GNU General Public License (v2 or later) Description: A set of Python modules and utility programmes to deal with Replay Gain information -- calculate it (with GStreamer), read and write it (with Mutagen). It has support for Ogg Vorbis (or probably anything stored in an Ogg container), Flac, WavPack (oddly enough) and MP3 (in different incarnations). There‘s also a command-line programme, ``replaygain``, that works very similar to its like- named cousins, most prominently ``vorbisgain`` and ``mp3gain`` -- only that it works for all those supported formats alike. ``collectiongain`` on the other hand is a kind of fire-and-forget tool for big amounts of music files. Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Environment :: Console Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Programming Language :: Python :: 2 Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires: pygobject Requires: mutagen rgain-1.2/COPYING0000664000175000017500000004325412141205302012531 0ustar fkfk00000000000000 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. rgain-1.2/man/0000775000175000017500000000000012141224140012242 5ustar fkfk00000000000000rgain-1.2/man/replaygain.rst0000664000175000017500000000736712141221451015146 0ustar fkfk00000000000000============ replaygain ============ -------------------------------- single file Replay Gain editor -------------------------------- :Date: 2011-11-26 :Version: 1.2 :Manual section: 1 :Manual group: rgain SYNOPSIS ======== | **replaygain** [*options*] *AUDIO_FILE* [*AUDIO_FILE* ...] | **replaygain** --help | **replaygain** --version DESCRIPTION =========== **replaygain** applies or displays Replay Gain information for audio files. OPTIONS ======= --version Display the version of the software. -h, --help Display a short documentation. -f, --force Recalculate Replay Gain even if the file already contains gain information. -d, --dry-run Don't actually modify any files. -r REF, --reference-loudness=REF Set the reference loudness to REF dB (default: 89 dB) --mp3-format=MP3_FORMAT Choose the Replay Gain data format for MP3 files. The default setting should be compatible with most decent software music players, so it is generally not necessary to mess with this setting. See below for more information. --no-album Don't write any album gain information. --show Don't calculate anything, simply show Replay Gain information for the specified files. In this mode, all options other than **--mp3-format** are ignored. MP3 formats =========== Proper Replay Gain support for MP3 files is a bit of a mess: on the one hand, there is the **mp3gain** application [1] which was relatively widely used (I don't know if it still is) -- it directly modifies the audio data which has the advantage that it works with pretty much any player, but it also means you have to decide ahead of time whether you want track gain or album gain. Besides, it's just not very elegant. On the other hand, there are at least two commonly used ways to store proper Replay Gain information in ID3v2 tags [2]. Now, in general you don't have to worry about this when using this package: by default, **replaygain** and **collectiongain** will read and write Replay Gain information in the two most commonly used formats. However, if for whatever reason you need more control over the MP3 Replay Gain information, you can use the **--mp3-format** option (supported by both programs) to change the behaviour. Possible choices with this switch are: - *replaygain.org* (alias: *fb2k*) Replay Gain information is stored in ID3v2 TXXX frames. This format is specified on the replaygain.org website as the recommended format for MP3 files. Notably, this format is also used by the foobar2000 music player for Windows [3]. - *legacy* (alias: *ql*) Replay Gain information is stored in ID3v2.4 RVA2 frames. This format is described as "legacy" by replaygain.org; however, it is still the primary format for at least the Quod Libet music player [4] and possibly others. It should be noted that this format does not support volume adjustments of more than 64 dB: if the calculated gain value is smaller than -64 dB or greater than or equal to +64 dB, it is clamped to these limit values. - *default* This is the default implementation used by both **replaygain** and **collectiongain**. When writing Replay Gain data, both the *replaygain.org* as well as the *legacy* format are written. As for reading, if a file contains data in both formats, both data sets are read and then compared. If they match up, that Replay Gain information is returned for the file. However, if they don't match, no Replay Gain data is returned to signal that this file does not contain valid (read: consistent) Replay Gain information. [1] http://mp3gain.sourceforce.net [2] http://wiki.hydrogenaudio.org/index.php?title=ReplayGain_specification#ID3v2 [3] http://foobar2000.org [4] http://code.google.com/p/quodlibet SEE ALSO ======== **collectiongain(1)** rgain-1.2/man/collectiongain.rst0000664000175000017500000001003012141221451015762 0ustar fkfk00000000000000================ collectiongain ================ ------------------------------------------ large scale Replay Gain calculating tool ------------------------------------------ :Date: 2011-11-26 :Version: 1.2 :Manual section: 1 :Manual group: rgain SYNOPSIS ======== | **collectiongain** [*options*] *music_dir* | **collectiongain** --help | **collectiongain** --version DESCRIPTION =========== **collectiongain** is a script calculating the Replay Gain values of a large set of music files inside *music_dir*. Each audio file will be rectified against the other files of the same album, which are identified using the file tags. OPTIONS ======= --version Display the version of the software. -h, --help Display a short summary of the available options. -f, --force Recalculate Replay Gain even if the file already contains gain information. -d, --dry-run Don't actually modify any files. -r REF, --reference-loudness=REF Set the reference loudness to REF dB (default: 89 dB) --mp3-format=MP3_FORMAT Choose the Replay Gain data format for MP3 files. The default setting should be compatible with most decent software music players, so it is generally not necessary to mess with this setting. See below for more information. --ignore-cache Don't trust implicit assumptions about what was already done, instead check all files for Replay Gain data explicitly. -j JOBS, --jobs=JOBS Run JOBS jobs simultaneously. Must be >= 1. By default, this is set to the number of CPU cores in the system to provide best performance. MP3 formats =========== Proper Replay Gain support for MP3 files is a bit of a mess: on the one hand, there is the **mp3gain** application [1] which was relatively widely used (I don't know if it still is) -- it directly modifies the audio data which has the advantage that it works with pretty much any player, but it also means you have to decide ahead of time whether you want track gain or album gain. Besides, it's just not very elegant. On the other hand, there are at least two commonly used ways to store proper Replay Gain information in ID3v2 tags [2]. Now, in general you don't have to worry about this when using this package: by default, **replaygain** and **collectiongain** will read and write Replay Gain information in the two most commonly used formats. However, if for whatever reason you need more control over the MP3 Replay Gain information, you can use the **--mp3-format** option (supported by both programs) to change the behaviour. Possible choices with this switch are: - *replaygain.org* (alias: *fb2k*) Replay Gain information is stored in ID3v2 TXXX frames. This format is specified on the replaygain.org website as the recommended format for MP3 files. Notably, this format is also used by the foobar2000 music player for Windows [3]. - *legacy* (alias: *ql*) Replay Gain information is stored in ID3v2.4 RVA2 frames. This format is described as "legacy" by replaygain.org; however, it is still the primary format for at least the Quod Libet music player [4] and possibly others. It should be noted that this format does not support volume adjustments of more than 64 dB: if the calculated gain value is smaller than -64 dB or greater than or equal to +64 dB, it is clamped to these limit values. - *default* This is the default implementation used by both **replaygain** and **collectiongain**. When writing Replay Gain data, both the *replaygain.org* as well as the *legacy* format are written. As for reading, if a file contains data in both formats, both data sets are read and then compared. If they match up, that Replay Gain information is returned for the file. However, if they don't match, no Replay Gain data is returned to signal that this file does not contain valid (read: consistent) Replay Gain information. [1] http://mp3gain.sourceforce.net [2] http://wiki.hydrogenaudio.org/index.php?title=ReplayGain_specification#ID3v2 [3] http://foobar2000.org [4] http://code.google.com/p/quodlibet SEE ALSO ======== **replaygain(1)** rgain-1.2/rgain/0000775000175000017500000000000012141224140012567 5ustar fkfk00000000000000rgain-1.2/rgain/__init__.py0000664000175000017500000000347012141205302014703 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2012 Felix Krull # # 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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. class GainData(object): """A class that contains Replay Gain data. Arguments for ``__init__`` are also instance variables. These are: - ``gain``: the gain (in dB, relative to ``ref_level``) - ``peak``: the peak - ``ref_level``: the used reference level (in dB) """ def __init__(self, gain, peak=1.0, ref_level=89): self.gain = gain self.peak = peak self.ref_level = ref_level def __str__(self): return ("gain=%.2f dB; peak=%.8f; reference-level=%i dB" % (self.gain, self.peak, self.ref_level)) def __eq__(self, other): return other is not None and (self.gain == other.gain and self.peak == other.peak and self.ref_level == other.ref_level) class GSTError(Exception): def __init__(self, gerror, debug): self.domain = gerror.domain self.code = gerror.code self.message = gerror.message self.debug = debug def __unicode__(self): return u"GST error: %s (%s)" % (self.message, self.debug) rgain-1.2/rgain/rgcalc.py0000664000175000017500000002174612141222200014401 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2013 Felix Krull # # 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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """Replay Gain analysis using GStreamer. See ``ReplayGain`` class for full documentation or use the ``calculate`` function. """ import os.path import gi gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst from rgain import GainData, GSTError, util # Make sure GObject threads don't crash GObject.threads_init() def to_utf8(string): if isinstance(string, unicode): return string.encode("utf-8") else: return string.decode("utf-8").encode("utf-8") class ReplayGain(GObject.GObject): """Perform a Replay Gain analysis on some files. This class doesn't actually write any Replay Gain information - that is left as an exercise to the user. It only analyzes the files and presents the result. Basic usage is as follows: - instantiate the class, passing it a list of file names and optionally the reference loudness level to use (defaults to 89 dB), - connect to the signals the class provides, - get yourself a glib main loop (e.g. ``GObject.MainLoop`` or the one from GTK), - call ``replaygain_instance.start()`` to start processing, - start your main loop to dispatch events and - wait. Once you've done that, you can retrieve the data from ``track_data`` (which is a dict: keys are file names, values are ``GainData`` instances) and ``album_data`` (a 'GainData' instance, even though it may contain only ``None`` values if album gain isn't calculated). Note that the values don't contain any kind of unit, which might be needed. """ __gsignals__ = { "all-finished": (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT)), "track-started": (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)), "track-finished": (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING, GObject.TYPE_PYOBJECT)), "error": (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,)), } def __init__(self, files, force=False, ref_lvl=89): GObject.GObject.__init__(self) self.files = files self.force = force self.ref_lvl = ref_lvl self._setup_pipeline() self._setup_rg_elem() self._files_iter = iter(self.files) # this holds all track gain data self.track_data = {} self.album_data = GainData(0) def start(self): """Start processing. For it to work correctly, you'll need to run some GObject main loop (e.g. the Gtk one) or process any events manually (though I have no idea how or if that works). """ if not self._next_file(): raise ValueError("do never, ever run this thing without any files") self.pipe.set_state(Gst.State.PLAYING) def pause(self, pause): if pause: self.pipe.set_state(Gst.State.PAUSED) else: self.pipe.set_state(Gst.State.PLAYING) def stop(self): self.pipe.set_state(Gst.State.NULL) # internal stuff def _setup_pipeline(self): """Setup the pipeline.""" self.pipe = Gst.Pipeline() # elements self.src = Gst.ElementFactory.make("filesrc", "src") self.pipe.add(self.src) self.decbin = Gst.ElementFactory.make("decodebin", "decbin") self.pipe.add(self.decbin) self.conv = Gst.ElementFactory.make("audioconvert", "conv") self.pipe.add(self.conv) self.res = Gst.ElementFactory.make("audioresample", "res") self.pipe.add(self.res) self.rg = Gst.ElementFactory.make("rganalysis", "rg") self.pipe.add(self.rg) self.sink = Gst.ElementFactory.make("fakesink", "sink") self.pipe.add(self.sink) # Set num-tracks to the number of files we have to process so they're # all treated as one album. Fixes #8. self.rg.set_property("num-tracks", len(self.files)) # link self.src.link(self.decbin) self.conv.link(self.res) self.res.link(self.rg) self.rg.link(self.sink) self.decbin.connect("pad-added", self._on_pad_added) self.decbin.connect("pad-removed", self._on_pad_removed) bus = self.pipe.get_bus() bus.add_signal_watch() bus.connect("message", self._on_message) def _setup_rg_elem(self): # there's no way to specify 'forced', as it's usually useless self.rg.set_property("forced", True) self.rg.set_property("reference-level", self.ref_lvl) def _next_file(self): """Load the next file to analyze. Returns False if everything is done and the pipeline shouldn't be started again; True otherwise. """ # get the next file try: fname = self._files_iter.next() except StopIteration: self.emit("all-finished", self.track_data, self.album_data) return False # point the source to the new file self.src.set_property("location", to_utf8(fname)) self._current_file = fname self.emit("track-started", to_utf8(fname)) return True def _process_tags(self, msg): """Process a tag message.""" tags = msg.parse_tag() trackdata = self.track_data.setdefault(self._current_file, GainData(0)) def handle_tag(taglist, tag, userdata): if tag == Gst.TAG_TRACK_GAIN: _, trackdata.gain = taglist.get_double(tag) elif tag == Gst.TAG_TRACK_PEAK: _, trackdata.peak = taglist.get_double(tag) elif tag == Gst.TAG_REFERENCE_LEVEL: _, trackdata.ref_level = taglist.get_double(tag) elif tag == Gst.TAG_ALBUM_GAIN: _, self.album_data.gain = taglist.get_double(tag) elif tag == Gst.TAG_ALBUM_PEAK: _, self.album_data.peak = taglist.get_double(tag) tags.foreach(handle_tag, None) # event handlers def _on_pad_added(self, decbin, new_pad): sinkpad = self.conv.get_compatible_pad(new_pad, None) if sinkpad is not None: new_pad.link(sinkpad) def _on_pad_removed(self, decbin, old_pad): peer = old_pad.get_peer() if peer is not None: old_pad.unlink(peer) def _on_message(self, bus, msg): if msg.type == Gst.MessageType.TAG: self._process_tags(msg) elif msg.type == Gst.MessageType.EOS: self.emit("track-finished", to_utf8(self._current_file), self.track_data[self._current_file]) # Preserve rganalysis state self.rg.set_locked_state(True) self.pipe.set_state(Gst.State.NULL) ret = self._next_file() if ret: self.pipe.set_state(Gst.State.PLAYING) # For some reason, GStreamer 1.0's rganalysis element produces # an error here unless a flush has been performed. pad = self.rg.get_static_pad("src") pad.send_event(Gst.Event.new_flush_start()) pad.send_event(Gst.Event.new_flush_stop(True)) self.rg.set_locked_state(False) elif msg.type == Gst.MessageType.ERROR: self.pipe.set_state(Gst.State.NULL) err, debug = msg.parse_error() self.emit("error", GSTError(err, debug)) def calculate(*args, **kwargs): """Analyze some files. This is only a convenience interface to the ``ReplayGain`` class: it takes the same arguments, but setups its own main loop and returns the results once everything's finished. """ exc_slot = [None] def on_finished(evsrc, trackdata, albumdata): # all done loop.quit() def on_error(evsrc, exc): exc_slot[0] = exc loop.quit() rg = ReplayGain(*args, **kwargs) with util.gobject_signals(rg, ("all-finished", on_finished), ("error", on_error),): loop = GObject.MainLoop() rg.start() loop.run() if exc_slot[0] is not None: raise exc_slot[0] return (rg.track_data, rg.album_data) rgain-1.2/rgain/script/0000775000175000017500000000000012141224140014073 5ustar fkfk00000000000000rgain-1.2/rgain/script/__init__.py0000664000175000017500000001023612141221451016210 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2012 Felix Krull # # 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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. import sys from optparse import OptionParser import traceback import gi gi.require_version("Gst", "1.0") from gi.repository import Gst import rgain.rgio stdout_encoding = sys.stdout.encoding or sys.getfilesystemencoding() def ou(arg): if isinstance(arg, str): return arg.decode("ascii").encode(stdout_encoding) return arg.encode(stdout_encoding) def un(arg, encoding): if isinstance(arg, str): return arg.decode(encoding) return arg class Error(Exception): def __init__(self, message, exc_info=None): Exception.__init__(self, message) # as long as instances are only constructed in exception handlers, this # should get us what we want self.exc_info = exc_info if exc_info else sys.exc_info() def __unicode__(self): if not self._output_full_exception(): return Exception.__unicode__(self) else: return unicode(u"".join(traceback.format_exception(*self.exc_info))) def _output_full_exception(self): return self.exc_info[0] not in [IOError, rgain.rgio.AudioFormatError, rgain.GSTError] def init_gstreamer(): """Properly initialise GStreamer for the command-line interfaces. Specifically, GStreamer options are parsed and processed by GStreamer, but it is also kept from taking over the main help output (by pretending -h or --help wasn't passed, if necessary). --help-gst should be documented in the main help output as a switch to display GStreamer options.""" # Strip any --help options from the command line. stripped_options = [] for opt in ["-h", "--help"]: if opt in sys.argv: sys.argv.remove(opt) stripped_options.append(opt) # Then, pass any remaining options to GStreamer. sys.argv = Gst.init(sys.argv) # Finally, restore any help options so optparse can eat them. for opt in stripped_options: sys.argv.append(opt) def common_options(): opts = OptionParser(version="%prog 1.2") opts.add_option("-f", "--force", help="Recalculate Replay Gain even if the " "file already contains gain information.", dest="force", action="store_true") opts.add_option("-d", "--dry-run", help="Don't actually modify any files.", dest="dry_run", action="store_true") opts.add_option("-r", "--reference-loudness", help="Set the reference " "loudness to REF dB (default: %default dB)", metavar="REF", dest="ref_level", action="store", type="int") opts.add_option("--mp3-format", help="Choose the Replay Gain data format " "for MP3 files. The default setting should be compatible " "with most decent software music players, so it is " "generally not necessary to mess with this setting. Check " "the README or man page for more information.", dest="mp3_format", action="store", type="choice", choices=rgain.rgio.BaseFormatsMap.MP3_DISPLAY_FORMATS) # This option only exists to show up in the help output; if it's actually # specified, GStreamer should eat it. opts.add_option("--help-gst", help="Show GStreamer options.", dest="help_gst", action="store_true") opts.set_defaults(force=False, dry_run=False, ref_level=89, mp3_format="default") return opts rgain-1.2/rgain/script/collectiongain.py0000664000175000017500000003234412141205302017444 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2012 Felix Krull # # 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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. import contextlib import multiprocessing import cStringIO import sys import os.path try: from hashlib import md5 except ImportError: from md5 import new as md5 try: import cPickle as pickle except ImportError: import pickle import mutagen from mutagen.id3 import TXXX from rgain import rgio from rgain.script import ou, un, Error, common_options, init_gstreamer from rgain.script.replaygain import do_gain # all of collectiongain def relpath(path, base): size = len(base) if os.path.basename(base): # this means ``base`` does not end with a separator size += 1 return path[size:] def read_cache(cache_file): if not os.path.isfile(cache_file): files = {} else: try: f = open(cache_file, "rb") files = pickle.load(f) except Exception, exc: print ou(u"Error while reading the cache - %s" % exc) finally: try: f.close() except NameError: pass return files def write_cache(cache_file, files): cache_dir = os.path.dirname(cache_file) try: if not os.path.isdir(cache_dir): os.makedirs(cache_dir, 0755) f = open(cache_file, "wb") pickle.dump(files, f, 2) except Exception, exc: print ou(u"Error while writing the cache - %s" % exc) finally: try: f.close() except NameError: pass def validate_cache(files): for filepath, record in files.items(): if (isinstance(filepath, basestring) and hasattr(record, "__getitem__") and hasattr(record, "__len__") and len(record) == 3 and (isinstance(record[0], basestring) or record[0] is None) and (isinstance(record[1], int) or isinstance(record[1], float)) and isinstance(record[2], bool) ): continue else: # funny record, purge it del files[filepath] def get_album_id(music_dir, filepath): properpath = os.path.join(music_dir, filepath) ext = os.path.splitext(filepath)[1] try: tags = mutagen.File(properpath) except Exception, exc: raise Error(u"%s: %s" % (filepath, exc)) album_id = None if ext == ".mp3": for frame in tags.itervalues(): if isinstance(frame, TXXX) and frame.desc == "MusicBrainz Album Id": album_id = frame.text[0] break else: try: album_id = tags.get("musicbrainz_albumid")[0] except TypeError: pass if album_id is not None: return album_id if ext == ".mp3": if "TALB" in tags: album = tags["TALB"].text[0] else: album = None else: album = tags.get("album", [""])[0] if album: if ext == ".mp3": artist = None for frame in tags.itervalues(): if isinstance(frame, TXXX) and "albumartist" in frame.desc: # these heuristics are a bit fragile artist = frame.text[0] break if not artist: # TODO: is this correct? if "TPE1" in tags: artist = tags["TPE1"].text[0] else: artist = tags.get("albumartist") or tags.get("artist") if artist: artist = artist[0] if not artist: artist = u"" album_id = u"%s - %s" % (artist, album) else: album_id = None return album_id def collect_files(music_dir, files, cache, supported_formats): i = 0 for dirpath, dirnames, filenames in os.walk(music_dir): for filename in filenames: filepath = un(relpath(os.path.join(dirpath, filename), music_dir), sys.getfilesystemencoding()) properpath = os.path.join(dirpath, filename) mtime = os.path.getmtime(properpath) # check the cache if filepath in cache: cache[filepath] = True record = files[filepath] if mtime <= record[1]: # the file's still ok continue ext = os.path.splitext(filename)[1] if ext in supported_formats: i += 1 print ou(u" [%i] %s |" % (i, filepath)), album_id = get_album_id(music_dir, filepath) print ou(album_id or u"") # fields here: album_id, mtime, already_processed files[filepath] = (album_id, mtime, False) def transform_cache(files, ignore_cache=False): # transform ``files`` into a usable data structure albums = {} single_tracks = [] for filepath, (album_id, mtime, processed) in files.iteritems(): if album_id is not None: albums.setdefault(album_id, []).append(filepath) else: single_tracks.append(filepath) # purge anything that's marked as processed, if desired if not ignore_cache: for album_id, album_files in albums.items(): keep = False for filepath in album_files: if not files[filepath][2]: keep = True break if not keep: del albums[album_id] for filepath in single_tracks[:]: if files[filepath][2]: single_tracks.remove(filepath) return albums, single_tracks def update_cache(files, music_dir, tracks, album_id): for filepath in tracks: mtime = os.path.getmtime(os.path.join(music_dir, filepath)) files[filepath] = (album_id, mtime, True) @contextlib.contextmanager def stdstreams(stdout, stderr): old_stdout = sys.stdout old_stderr = sys.stderr sys.stdout = stdout sys.stderr = stderr try: yield finally: sys.stdout = old_stdout sys.stderr = old_stderr def do_gain_async(queue, job_key, files, ref_level, force, dry_run, album, mp3_format): output = cStringIO.StringIO() try: with stdstreams(output, output): if album: print ou(u"%s:" % job_key[1]), do_gain(files, ref_level, force, dry_run, album, mp3_format) print except Exception, exc: # We can't reliably serialise and pass the exception information to the # driver process so we stringify it here. queue.put((job_key, output.getvalue(), unicode(exc))) else: queue.put((job_key, output.getvalue(), None)) def do_gain_all(music_dir, albums, single_tracks, files, ref_level=89, force=False, dry_run=False, mp3_format=None, jobs=0, stop_on_error=False): pool = multiprocessing.Pool(None if jobs == 0 else jobs) manager = multiprocessing.Manager() queue = manager.Queue() num_jobs = 0 print "Dispatching jobs ..." if single_tracks: pool.apply_async(do_gain_async, [queue, (single_tracks, None), [os.path.join(music_dir, path) for path in single_tracks], ref_level, force, dry_run, False, mp3_format]) num_jobs += 1 for album_id, album_files in albums.iteritems(): #print ou(u"%s:" % album_id), pool.apply_async(do_gain_async, [queue, (album_files, album_id), [os.path.join(music_dir, path) for path in album_files], ref_level, force, dry_run, True, mp3_format]) num_jobs += 1 pool.close() print "Now waiting for results ..." failed_jobs = [] try: all_jobs = num_jobs successful = 0 while num_jobs > 0: job_key, output, exc = queue.get() num_jobs -= 1 if exc: failed_jobs.append((job_key, output, exc)) else: successful += 1 print output.strip() print "Successfully finished %s of %s." % (successful, all_jobs) print # Update cache. if not dry_run: tracks, album_id = job_key update_cache(files, music_dir, tracks, album_id) finally: try: pool.terminate() except Exception: # terminate ends rather horribly so we just silence it. pass if len(failed_jobs) > 0: print "Unfortunately, there were some errors:" for key, output, exc in failed_jobs: print output.strip() print >> sys.stderr, ou(exc) print print "%s successful, %s failed." % (successful, len(failed_jobs)) def do_collectiongain(music_dir, ref_level=89, force=False, dry_run=False, mp3_format=None, ignore_cache=False, jobs=0): music_dir = un(music_dir, sys.getfilesystemencoding()) music_abspath = os.path.abspath(music_dir) musicpath_hash = md5(music_abspath.encode("utf-8")).hexdigest() cache_file = os.path.join(os.path.expanduser("~"), ".cache", "collectiongain-cache.%s" % musicpath_hash) # load the cache files = read_cache(cache_file) # yeah, side-effects are bad, I know validate_cache(files) cache = dict.fromkeys(files.iterkeys(), False) print "Collecting files ..." # whenever this part is stopped (KeyboardInterrupt/other exception), the # cache is written to disk so all progress persists try: collect_files(music_dir, files, cache, rgio.BaseFormatsMap(mp3_format).supported_formats) # clean cache for filepath, visited in cache.items(): if not visited: del cache[filepath] del files[filepath] # hopefully gets rid of at least one huge data structure del cache albums, single_tracks = transform_cache(files, ignore_cache) # gain everything that has survived the cleansing do_gain_all(music_dir, albums, single_tracks, files, ref_level, force, dry_run, mp3_format, jobs) finally: validate_cache(files) write_cache(cache_file, files) print "All finished." def collectiongain_options(): opts = common_options() opts.add_option("--ignore-cache", help="Don't trust implicit assumptions " "about what was already done, instead check all files for " "Replay Gain data explicitly.", dest="ignore_cache", action="store_true") opts.add_option("-j", "--jobs", help="Specifies the number of jobs to run " "simultaneously. Must be >= 1. By default, this is set to " "the number of CPU cores in the system to provide best " "performance.", dest="jobs", action="store", type="int") opts.set_defaults(ignore_cache=False, jobs=None) opts.set_usage("%prog [options] MUSIC_DIR") opts.set_description("Calculate Replay Gain for a large set of audio files " "without asking many questions. This program " "calculates an album ID for any audo file in " "MUSIC_DIR. Then, album gain will be applied to all " "files with the same album ID. The album ID is " "created from file tags as follows: If an 'album' tag " "is present, it is joined with the contents of an " "'albumartist' tag, or, if that isn't set, the " "contents of the 'artist' tag, or nothing if there is " "no 'artist' tag as well. On the other hand, if no " "'album' tag is present, the file is assumed to be a " "single track without album; in that case, no album " "gain will be calculated for that file.") return opts def collectiongain(): init_gstreamer() optparser = collectiongain_options() opts, args = optparser.parse_args() if len(args) != 1: optparser.error("pass one directory path") if opts.jobs is not None and opts.jobs < 1: optparser.error("jobs must be at least 1") try: do_collectiongain(args[0], opts.ref_level, opts.force, opts.dry_run, opts.mp3_format, opts.ignore_cache, opts.jobs) except Error, exc: print print >> sys.stderr, ou(unicode(exc)) sys.exit(1) except KeyboardInterrupt: print "Interrupted." if __name__ == "__main__": collectiongain() rgain-1.2/rgain/script/replaygain.py0000664000175000017500000001661312141205302016606 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2012 Felix Krull # # 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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. import sys import os.path from gi.repository import GObject from rgain import rgcalc, rgio, util from rgain.script import ou, un, Error, common_options, init_gstreamer # calculate the gain for the given files def calculate_gain(files, ref_level): exc_slot = [None] # handlers def on_finished(evsrc, trackdata, albumdata): loop.quit() def on_trk_started(evsrc, filename): print ou(" %s:" % filename.decode("utf-8")), sys.stdout.flush() def on_trk_finished(evsrc, filename, gaindata): if gaindata: print "%.2f dB" % gaindata.gain else: print "done" def on_error(evsrc, exc): exc_slot[0] = exc loop.quit() rg = rgcalc.ReplayGain(files, True, ref_level) with util.gobject_signals(rg, ("all-finished", on_finished), ("track-started", on_trk_started), ("track-finished", on_trk_finished), ("error", on_error),): loop = GObject.MainLoop() rg.start() loop.run() if exc_slot[0] is not None: raise exc_slot[0] return rg.track_data, rg.album_data def do_gain(files, ref_level=89, force=False, dry_run=False, album=True, mp3_format=None): files = [un(filename, sys.getfilesystemencoding()) for filename in files] formats_map = rgio.BaseFormatsMap(mp3_format) newfiles = [] for filename in files: if not os.path.splitext(filename)[1] in formats_map.supported_formats: print ou(u"%s: not supported, ignoring it" % filename) else: newfiles.append(filename) files = newfiles if not force: print "Checking for Replay Gain information ..." newfiles = [] for filename in files: print ou(u" %s:" % filename), try: trackdata, albumdata = formats_map.read_gain(filename) except Exception, exc: raise Error(u"%s: %s" % (filename, exc)) else: if trackdata and albumdata: print "track and album" elif not trackdata and albumdata: print "album only" newfiles.append(filename) elif trackdata and not albumdata: print "track only" if album: newfiles.append(filename) else: print "none" newfiles.append(filename) if not album: files = newfiles elif not len(newfiles): files = newfiles if not files: # no files left print "Nothing to do." return 0 # calculate gain print "Calculating Replay Gain information ..." try: tracks_data, albumdata = calculate_gain(files, ref_level) if album: print " Album gain: %.2f dB" % albumdata.gain except Exception, exc: raise Error(u"Error while calculating gain - %s" % exc) if not album: albumdata = None # write gain if not dry_run: print "Writing Replay Gain information to files ..." for filename, trackdata in tracks_data.iteritems(): print ou(u" %s:" % filename), try: formats_map.write_gain(filename, trackdata, albumdata) except Exception, exc: raise Error(u"%s: %s" % (filename, exc)) else: print "done" print "Done" # a simple Replay Gain dump def show_rgain_info(filenames, mp3_format=None): formats_map = rgio.BaseFormatsMap(mp3_format) for filename in filenames: filename = un(filename, sys.getfilesystemencoding()) print ou(filename) try: trackdata, albumdata = formats_map.read_gain(filename) except Exception, exc: print " " % exc continue if not trackdata and not albumdata: print " " if trackdata and trackdata.ref_level: ref_level = trackdata.ref_level elif albumdata and albumdata.ref_level: ref_level = albumdata.ref_level else: ref_level = None if ref_level is not None: print " Reference loudness %i dB" % ref_level if trackdata: print " Track gain %.2f dB" % trackdata.gain print " Track peak %.8f" % trackdata.peak if albumdata: print " Album gain %.2f dB" % albumdata.gain print " Album peak %.8f" % albumdata.peak def rgain_options(): opts = common_options() opts.add_option("--no-album", help="Don't write any album gain " "information.", dest="album", action="store_false") opts.add_option("--show", help="Don't calculate anything, simply show " "Replay Gain information for the specified files. In this " "mode, all other options save for '--mp3-format' are " "ignored, for they would make no sense.", dest="show", action="store_true") opts.set_defaults(album=True, show=False) opts.set_usage("%prog [options] AUDIO_FILE [AUDIO_FILE ...]") opts.set_description("Apply or display Replay Gain information for audio " "files. This program is similar to the likes of " "'vorbisgain' or 'mp3gain': You pass in some files, " "they are analyzed and receive their share of Replay " "Gain. The difference is that '%prog' supports " "several file formats, namely Ogg Vorbis (anything " "you'd put into an Ogg container, actually), Flac, " "WavPack and MP3. Also, it allows you to view " "existing Replay Gain information in any of those " "file types.") return opts def replaygain(): init_gstreamer() optparser = rgain_options() opts, args = optparser.parse_args() if not args: optparser.error("pass one or several audio file names") if opts.show: show_rgain_info(args, opts.mp3_format) else: try: do_gain(args, opts.ref_level, opts.force, opts.dry_run, opts.album, opts.mp3_format) except Error, exc: print print >> sys.stderr, ou(unicode(exc)) sys.exit(1) except KeyboardInterrupt: print "Interrupted." if __name__ == "__main__": replaygain() rgain-1.2/rgain/util.py0000664000175000017500000000222612141205302014117 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2012 Felix Krull # # 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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. import contextlib @contextlib.contextmanager def gobject_signals(obj, *signals): """Context manager to connect and disconnect GObject signals using a ``with`` statement. """ signal_ids = [] try: for signal in signals: signal_ids.append(obj.connect(*signal)) yield finally: for signal_id in signal_ids: obj.disconnect(signal_id) rgain-1.2/rgain/rgio.py0000664000175000017500000002655712141205302014117 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2012 Felix Krull # # 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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. from itertools import combinations import os.path import warnings import mutagen from mutagen.id3 import ID3, RVA2, TXXX from mutagen.apev2 import APEv2File from rgain import GainData class AudioFormatError(Exception): def __init__(self, filename): Exception.__init__(self, u"Did not understand file: %s" % filename) def parse_db(string): string = string.strip() if string.lower().endswith("db"): string = string[:-2].strip() try: db = float(string) except ValueError: db = None return db def parse_peak(string): try: peak = float(string.strip()) except ValueError: peak = None return peak def almost_equal(a, b, epsilon): if a is None and b is None: return True elif a is None or b is None: return False return abs(a - b) <= epsilon # basic tag-based reader/writer, suited for Ogg (Vorbis, Flac, Speex, ...) and # Flac files at least (also WavPack it seems) def rg_read_gain(filename): tags = mutagen.File(filename) if tags is None: raise AudioFormatError(filename) def read_gain_data(desc): gain_tag = u"replaygain_%s_gain" % desc peak_tag = u"replaygain_%s_peak" % desc if gain_tag in tags: gain = parse_db(tags[gain_tag][0]) if gain is None: return None gaindata = GainData(gain) if peak_tag in tags: peak = parse_peak(tags[peak_tag][0]) if peak is not None: gaindata.peak = peak if u"replaygain_reference_loudness" in tags: ref_level = parse_db(tags[u"replaygain_reference_loudness"][0]) if ref_level is not None: gaindata.ref_level = ref_level else: gaindata = None return gaindata return read_gain_data("track"), read_gain_data("album") def rg_write_gain(filename, trackdata, albumdata): tags = mutagen.File(filename) if tags is None: raise AudioFormatError(filename) if trackdata: tags[u"replaygain_track_gain"] = u"%.8f dB" % trackdata.gain tags[u"replaygain_track_peak"] = u"%.8f" % trackdata.peak tags[u"replaygain_reference_loudness"] = u"%i dB" % trackdata.ref_level if albumdata: tags[u"replaygain_album_gain"] = u"%.8f dB" % albumdata.gain tags[u"replaygain_album_peak"] = u"%.8f" % albumdata.peak tags.save() # ID3v2 support for legacy RVA2-frames-based format according to # http://wiki.hydrogenaudio.org/index.php?title=ReplayGain_specification#ID3v2 REFERENCE_LOUDNESS_TAGS = [u"replaygain_reference_loudness", u"QuodLibet::replaygain_reference_loudness"] def mp3_legacy_read_gain(filename): tags = ID3(filename) if tags is None: raise AudioFormatError(filename) def read_gain_data(desc): tag = u"RVA2:%s" % desc if tag in tags: frame = tags[tag] gaindata = GainData(frame.gain, frame.peak) # Read all supported reference loudness tags, using the first that # exists. for t in REFERENCE_LOUDNESS_TAGS: if t in ("TXXX:%s" % t for t in tags): gaindata.ref_level = parse_db(tags[t].text[0]) break else: gaindata = None return gaindata return read_gain_data("track"), read_gain_data("album") def clamp_rva2_gain(v): clamped = False if v < -64: v = -64 clamped = True if v >= 64: v = float(64 * 512 - 1) / 512.0 clamped = True if clamped: warnings.warn("gain value was out of bounds for RVA2 frame and was clamped to %.2f" % v) return v # I'm not sure if this situation could even reasonably happen, but # can't hurt to check, right? Right!? def clamp_rva2_peak(v): clamped = False if v < 0: v = 0 clamped = True if v >= 2: v = float(2**16 - 1) / float(2**15) clamped = True if clamped: warnings.warn("peak value was out of bounds for RVA2 frame and was clamped to %.5f" % v) return v def mp3_legacy_write_gain(filename, trackdata, albumdata): tags = ID3(filename) if tags is None: raise AudioFormatError(filename) if trackdata: trackgain = RVA2(desc=u"track", channel=1, gain=clamp_rva2_gain(trackdata.gain), peak=clamp_rva2_peak(trackdata.peak)) tags.add(trackgain) # write reference loudness tags for t in REFERENCE_LOUDNESS_TAGS: reflevel = TXXX(encoding=3, desc=t, text=[u"%i dB" % trackdata.ref_level]) tags.add(reflevel) if albumdata: albumgain = RVA2(desc=u"album", channel=1, gain=clamp_rva2_gain(albumdata.gain), peak=clamp_rva2_peak(albumdata.peak)) tags.add(albumgain) tags.save() # ID3v2 support for TXXX:replaygain_* frames as specified in # http://wiki.hydrogenaudio.org/index.php?title=ReplayGain_specification#ID3v2 # and as implemented by at least foobar2000. def mp3_rgorg_read_gain(filename): tags = ID3(filename) if tags is None: raise AudioFormatError(filename) def read_gain_data(desc): gain_tag = u"TXXX:replaygain_%s_gain" % desc peak_tag = u"TXXX:replaygain_%s_peak" % desc if gain_tag in tags: gain = parse_db(tags[gain_tag].text[0]) if gain is None: return None gaindata = GainData(gain) if peak_tag in tags: peak = parse_peak(tags[peak_tag].text[0]) if peak is not None: gaindata.peak = peak if u"TXXX:replaygain_reference_loudness" in tags: ref_level = parse_db(tags[ u"TXXX:replaygain_reference_loudness" ].text[0]) if ref_level is not None: gaindata.ref_level = ref_level else: gaindata = None return gaindata return read_gain_data("track"), read_gain_data("album") def mp3_rgorg_write_gain(filename, trackdata, albumdata): tags = ID3(filename) if tags is None: raise AudioFormatError(filename) def write_gain_data(desc, gaindata): gain_frame = TXXX(encoding=3, desc=u"replaygain_%s_gain" % desc, text=[u"%.8f dB" % gaindata.gain]) tags.add(gain_frame) peak_frame = TXXX(encoding=3, desc=u"replaygain_%s_peak" % desc, text=[u"%.8f" % gaindata.peak]) tags.add(peak_frame) if trackdata: write_gain_data("track", trackdata) tags.add(TXXX(encoding=3, desc=u"replaygain_reference_loudness", text=[u"%i dB" % trackdata.ref_level])) if albumdata: write_gain_data("album", albumdata) tags.save() # Special compatible MP3 support that # - reads both rg.org and legacy gain, compares them, returns them if they # match, else returns no gain data # - writes both rg.org and legacy gain GAIN_EPSILON = 0.1 PEAK_EPSILON = 0.001 REF_LEVEL_EPSILON = 0.1 # For these three functions, b is always the legacy values, i.e. the potentially # clamped ones. def gain_almost_equal(a, b): return (almost_equal(a, b, GAIN_EPSILON) or almost_equal(clamp_rva2_gain(a), b, GAIN_EPSILON)) def peak_almost_equal(a, b): return (almost_equal(a, b, PEAK_EPSILON) or almost_equal(clamp_rva2_peak(a), b, PEAK_EPSILON)) def gaindata_almost_equal(a, b): # Ensure neither element is None. if a is None: return b is None if b is None: return a is None if a == b: return True with warnings.catch_warnings(): warnings.filterwarnings("ignore") return (gain_almost_equal(a.gain, b.gain) and peak_almost_equal(a.peak, b.peak) and almost_equal(a.ref_level, b.ref_level, REF_LEVEL_EPSILON)) def mp3_default_read_gain(filename): rgorg = mp3_rgorg_read_gain(filename) legacy = mp3_legacy_read_gain(filename) # We want to ensure we have all bits of data so if we only have one format, # we say we have none to enforce recalculation. if rgorg[0] is None or legacy[0] is None: return (None, None) if (not gaindata_almost_equal(rgorg[0], legacy[0]) or not gaindata_almost_equal(rgorg[1], legacy[1])): # The different formats are not similar enough. return (None, None) else: return rgorg def mp3_default_write_gain(filename, trackdata, albumdata): mp3_legacy_write_gain(filename, trackdata, albumdata) mp3_rgorg_write_gain(filename, trackdata, albumdata) # code to pull everything together class UnknownFiletype(Exception): pass class BaseFormatsMap(object): BASE_MAP = { ".ogg": (rg_read_gain, rg_write_gain), ".oga": (rg_read_gain, rg_write_gain), ".flac": (rg_read_gain, rg_write_gain), ".wv": (rg_read_gain, rg_write_gain), } MP3_FORMATS = { None: (mp3_default_read_gain, mp3_default_write_gain), "default": (mp3_default_read_gain, mp3_default_write_gain), "replaygain.org": (mp3_rgorg_read_gain, mp3_rgorg_write_gain), "fb2k": (mp3_rgorg_read_gain, mp3_rgorg_write_gain), "legacy": (mp3_legacy_read_gain, mp3_legacy_write_gain), "ql": (mp3_legacy_read_gain, mp3_legacy_write_gain), } MP3_DISPLAY_FORMATS = ["default", "replaygain.org", "legacy", "ql", "fb2k"] MP3_DEFAULT_FORMAT = "default" def __init__(self, mp3_format=None, more_mappings=None): # yeah, you need to choose self.more_mappings = more_mappings if more_mappings else {} if mp3_format in self.MP3_FORMATS: self.more_mappings[".mp3"] = self.MP3_FORMATS[mp3_format] else: raise ValueError("invalid MP3 format %r" % mp3_format) @property def supported_formats(self): return (set(self.BASE_MAP.iterkeys()) | set(self.more_mappings.iterkeys())) def read_gain(self, filename): ext = os.path.splitext(filename)[1] if ext in self.more_mappings: accessor = self.more_mappings[ext] elif ext in self.BASE_MAP: accessor = self.BASE_MAP[ext] else: raise UnknownFiletype(ext) return accessor[0](filename) def write_gain(self, filename, trackgain, albumgain): ext = os.path.splitext(filename)[1] if ext in self.more_mappings: accessor = self.more_mappings[ext] elif ext in self.BASE_MAP: accessor = self.BASE_MAP[ext] else: raise UnknownFiletype(ext) accessor[1](filename, trackgain, albumgain) rgain-1.2/setup.py0000664000175000017500000000663212141221473013216 0ustar fkfk00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- from contextlib import closing import os import sys from distutils.core import Command, Distribution, setup from distutils.command.build import build from distutils.errors import DistutilsOptionError try: import docutils.core class ManpagesDistribution(Distribution): def __init__(self, attrs=None): self.rst_manpages = None Distribution.__init__(self, attrs) class build_manpages(Command): description = "Generate man pages." user_options = [ ("outputdir=", "b", "output directory for man pages"), ] def initialize_options(self): self.rst_manpages = None self.outputdir = None def finalize_options(self): if not self.outputdir: self.outputdir = os.path.join("build", "man") self.rst_manpages = self.distribution.rst_manpages def run(self): if not self.rst_manpages: return if not os.path.exists(self.outputdir): os.makedirs(self.outputdir, mode=0755) for infile, outfile in self.rst_manpages: print "Converting %s to %s ..." % (infile, outfile), docutils.core.publish_file(source_path=infile, destination_path=os.path.join(self.outputdir, outfile), writer_name="manpage") print "ok" build.sub_commands.append(("build_manpages", None)) manpages_args = { "rst_manpages": [ ("man/replaygain.rst", "replaygain.1"), ("man/collectiongain.rst", "collectiongain.1"), ], "cmdclass": {"build_manpages": build_manpages}, "distclass": ManpagesDistribution, } except ImportError: print >> sys.stderr, ("You do not have docutils, the manpages won't be " "generated.") manpages_args = {} setup( name="rgain", version="1.2", description="Multi-format Replay Gain utilities", author="Felix Krull", author_email="f_krull@gmx.de", url="http://bitbucket.org/fk/rgain", license="GNU General Public License (v2 or later)", classifiers=[ "Development Status :: 6 - Mature", "Environment :: Console", "Intended Audience :: End Users/Desktop", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License (GPL)", "Programming Language :: Python :: 2", "Topic :: Multimedia :: Sound/Audio :: Analysis", "Topic :: Software Development :: Libraries :: Python Modules", ], long_description="""\ A set of Python modules and utility programmes to deal with Replay Gain information -- calculate it (with GStreamer), read and write it (with Mutagen). It has support for Ogg Vorbis (or probably anything stored in an Ogg container), Flac, WavPack (oddly enough) and MP3 (in different incarnations). There‘s also a command-line programme, ``replaygain``, that works very similar to its like- named cousins, most prominently ``vorbisgain`` and ``mp3gain`` -- only that it works for all those supported formats alike. ``collectiongain`` on the other hand is a kind of fire-and-forget tool for big amounts of music files. """, packages=["rgain", "rgain.script"], scripts=["scripts/replaygain", "scripts/collectiongain"], requires=["pygobject", "mutagen"], **manpages_args ) rgain-1.2/scripts/0000775000175000017500000000000012141224140013156 5ustar fkfk00000000000000rgain-1.2/scripts/collectiongain0000775000175000017500000000022412141205302016073 0ustar fkfk00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- from rgain.script.collectiongain import collectiongain if __name__ == "__main__": collectiongain() rgain-1.2/scripts/replaygain0000775000175000017500000000021012141205302015227 0ustar fkfk00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- from rgain.script.replaygain import replaygain if __name__ == "__main__": replaygain() rgain-1.2/MANIFEST.in0000664000175000017500000000006112141205302013221 0ustar fkfk00000000000000include COPYING include MANIFEST.in include man/*