rgain-1.3.3/0000775000175000017500000000000012415577371011656 5ustar fkfk00000000000000rgain-1.3.3/README0000664000175000017500000002067512415577262012547 0ustar fkfk00000000000000rgain -- 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 You will also need GStreamer decoding plugins for any audio formats you want to use. 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 - MP4 (commonly using the AAC codec) - 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 * a MusicBrainz album artist ID, if that exists * an *albumartist* tag, if that exists, * or the *artist* tag * or nothing if none of the above tags exist. 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 pass the **ignore-cache** switch to make **collectiongain** totally ignore the cache; in that case, it will behave as if no cache was present and read your collection from scratch. 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.3.3 (2014-10-09) ------------------------ - Fixed swapped album gain and track peak tags. rgain 1.3.2 (2014-05-24) ------------------------ - Fixed some problems with non-UTF8 file names. They should now work as long as any file names touched by **rgain** match the system encoding. (https://bitbucket.org/fk/rgain/issue/12/unicodedecodeerror-ascii-codec-cant-decode) - Misc. bug fixes. rgain 1.3.1 (2013-11-29) ------------------------ - Support MP4/AAC (courtesy of Yevgeny Bezman). rgain 1.3 (2013-10-28) ---------------------- - Work around a bug in some pygobject 3.10 releases (https://bugzilla.gnome.org/show_bug.cgi?id=710447) - Properly recognise file extensions even with different capitalisation. - Overhaul album ID algorithm to be hopefully better at grouping files that belong to an album and, conversely, not mis-grouping files. Note that this change will invalidate any cache files you might still have so your entire collection will be re-scanned next time you run **collectiongain**. - Assorted bug fixes. rgain 1.2.1 (2013-10-18) ------------------------ - Fix issue with reading MP3 reference loudness tags. 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-2014 Felix Krull The manpages were originally written for the Debian project and are:: Copyright (c) 2011 Simon Chopin Copyright (c) 2012-2014 Felix Krull rgain-1.3.3/scripts/0000775000175000017500000000000012415577371013345 5ustar fkfk00000000000000rgain-1.3.3/scripts/replaygain0000775000175000017500000000021012415574725015417 0ustar fkfk00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- from rgain.script.replaygain import replaygain if __name__ == "__main__": replaygain() rgain-1.3.3/scripts/collectiongain0000775000175000017500000000022412415574725016263 0ustar fkfk00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- from rgain.script.collectiongain import collectiongain if __name__ == "__main__": collectiongain() rgain-1.3.3/rgain.egg-info/0000775000175000017500000000000012415577371014450 5ustar fkfk00000000000000rgain-1.3.3/rgain.egg-info/SOURCES.txt0000664000175000017500000000063612415577371016341 0ustar fkfk00000000000000COPYING MANIFEST.in README setup.py man/collectiongain.rst man/replaygain.rst rgain/__init__.py rgain/albumid.py rgain/rgcalc.py rgain/rgio.py rgain/util.py rgain/version.py rgain.egg-info/PKG-INFO rgain.egg-info/SOURCES.txt rgain.egg-info/dependency_links.txt rgain.egg-info/top_level.txt rgain/script/__init__.py rgain/script/collectiongain.py rgain/script/replaygain.py scripts/collectiongain scripts/replaygainrgain-1.3.3/rgain.egg-info/top_level.txt0000664000175000017500000000000612415577371017176 0ustar fkfk00000000000000rgain rgain-1.3.3/rgain.egg-info/PKG-INFO0000664000175000017500000000261412415577371015550 0ustar fkfk00000000000000Metadata-Version: 1.1 Name: rgain Version: 1.3.3 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, MP4 (aka AAC) 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 itworks 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.3.3/rgain.egg-info/dependency_links.txt0000664000175000017500000000000112415577371020516 0ustar fkfk00000000000000 rgain-1.3.3/setup.cfg0000664000175000017500000000007312415577371013477 0ustar fkfk00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 rgain-1.3.3/COPYING0000664000175000017500000004325412415574725012721 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.3.3/PKG-INFO0000664000175000017500000000261412415577371012756 0ustar fkfk00000000000000Metadata-Version: 1.1 Name: rgain Version: 1.3.3 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, MP4 (aka AAC) 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 itworks 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.3.3/setup.py0000664000175000017500000001252512415577017013372 0ustar fkfk00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- from __future__ import print_function from datetime import date import os import sys import tempfile try: from setuptools import Command, Distribution, setup except ImportError: print("setuptools unavailable, falling back to distutils.", file=sys.stderr) from distutils.core import Command, Distribution, setup from distutils.command.build import build from rgain import __version__ try: import docutils.core class ManpagesDistribution(Distribution): def __init__(self, attrs=None): self.rst_manpages = None self.rst_manpages_update_info = False self.rst_manpages_version = None self.rst_manpages_date = 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.rst_manpages_update_info = False self.rst_manpages_version = "1.0" self.rst_manpages_date = date.today() 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 self.rst_manpages_update_info = \ self.distribution.rst_manpages_update_info self.rst_manpages_version = self.distribution.rst_manpages_version self.rst_manpages_date = self.distribution.rst_manpages_date def run(self): if not self.rst_manpages: return if not os.path.exists(self.outputdir): os.makedirs(self.outputdir, mode=0o755) for infile, outfile in self.rst_manpages: if self.rst_manpages_update_info: print("Updating %s info..." % infile, end="") with tempfile.NamedTemporaryFile("w", delete=False) as tmp: with open(infile, "r") as f: for line in f: if line.startswith(":Date:"): dt = self.rst_manpages_date tmp.write( ":Date: %s-%s-%s\n" % (dt.year, dt.month, dt.day)) elif line.startswith(":Version:"): tmp.write(":Version: %s\n" % self.rst_manpages_version) else: tmp.write(line) real_infile = tmp.name print("ok") else: real_infile = infile print("Converting %s to %s ..." % (infile, outfile), end="") docutils.core.publish_file( source_path=real_infile, destination_path=os.path.join(self.outputdir, outfile), writer_name="manpage") if real_infile != infile: os.remove(real_infile) print("ok") build.sub_commands.append(("build_manpages", None)) manpages_args = { "rst_manpages": [ ("man/replaygain.rst", "replaygain.1"), ("man/collectiongain.rst", "collectiongain.1"), ], "rst_manpages_update_info": True, "rst_manpages_version": __version__, "rst_manpages_date": date.today(), "cmdclass": {"build_manpages": build_manpages}, "distclass": ManpagesDistribution, } except ImportError: print("docutils not found, manpages won't be generated.", file=sys.stderr) manpages_args = {} setup( name="rgain", version=__version__, 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, MP4 (aka AAC) 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 itworks 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.3.3/MANIFEST.in0000664000175000017500000000006112415574725013411 0ustar fkfk00000000000000include COPYING include MANIFEST.in include man/*rgain-1.3.3/rgain/0000775000175000017500000000000012415577371012756 5ustar fkfk00000000000000rgain-1.3.3/rgain/albumid.py0000664000175000017500000001020012415574725014736 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2014 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 mutagen from mutagen.id3 import ID3FileType def _mp3_get_frame_data(tags, frame_key, default=None): frame = tags.get(frame_key, None) if frame is not None: return frame.text[0] else: return default def _default_get_tag(tags, key, default=None): value = tags.get(key, None) if value is not None: return value[0] else: return default def _get_simple_tag(tags, mp3_key, default_key): if isinstance(tags, ID3FileType): return _mp3_get_frame_data(tags, mp3_key, None) else: return _default_get_tag(tags, default_key, None) get_musicbrainz_album_id = lambda tags: _get_simple_tag(tags, "TXXX:MusicBrainz Album Id", "musicbrainz_albumid") get_musicbrainz_albumartist_id = lambda tags: _get_simple_tag(tags, "TXXX:MusicBrainz Album Artist Id", "musicbrainz_albumartistid") get_musicbrainz_artist_id = lambda tags: _get_simple_tag(tags, "TXXX:MusicBrainz Artist Id", "musicbrainz_artistid") get_album = lambda tags: _get_simple_tag(tags, "TALB", "album") get_artist = lambda tags: _get_simple_tag(tags, "TPE1", "artist") def get_albumartist(tags): if isinstance(tags, ID3FileType): # We try several tags to get the album artist from an MP3 file: # - a generic "TXXX:albumartist" # - QL's "TXXX:QuodLibet::albumartist" # - fb2k's legacy "TXXX:ALBUM ARTIST" # - finally, "TPE2" as used by at least fb2k and Picard. According to # the ID3 standard, this is performer, but fb2k uses it for album # artist since 1.1.6, citing "compatibility with other players"; see # http://wiki.hydrogenaudio.org/index.php?title=Foobar2000:ID3_Tag_Mapping # - if none of these exist, we assume no album artist tag # All of these frame names are matched regardless of capitalisation. TAGS = [t.lower() for t in [ "TXXX:albumartist", "TXXX:QuodLibet::albumartist", "TXXX:ALBUM ARTIST", "TPE2"]] for key, frame in tags.iteritems(): if key.lower() in TAGS: return frame.text[0] # Nothing matched. return None else: # We just use the rather standard "albumartist" return _default_get_tag(tags, "albumartist", None) def _take_first_tag(tags, default, functions): for f in functions: value = f(tags) if value is not None: return value return default def get_album_id(tags): """Try to determine an album id based on the given tags. The basic logic is as follows: - If a MusicBrainz album ID exists, use that. - If an album tag exists, combine that with: - a MusicBrainz album artist ID if it exists, - otherwise an album artist tag if it exists, - otherwise an artist tag if it exists, - otherwise, nothing and use the result. - Otherwise, assume non-album track. """ mb_album_id = get_musicbrainz_album_id(tags) if mb_album_id is not None: return mb_album_id album = get_album(tags) if album is not None: artist_part = _take_first_tag(tags, None, [ get_musicbrainz_albumartist_id, get_albumartist, get_artist]) if artist_part is None: return album else: return u"%s - %s" % (artist_part, album) else: return None rgain-1.3.3/rgain/version.py0000664000175000017500000000002612415574725015013 0ustar fkfk00000000000000__version__ = "1.3.3" rgain-1.3.3/rgain/rgcalc.py0000664000175000017500000002540612415574725014572 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2014 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 sys import gi gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst from rgain import GainData, GSTError, util # Initialise threading. GObject.threads_init() # Also initialise threading. This hack is necessary because while threads_init # was originally deprecated in pygobject 3.10 and turned into a no-op, that # wouldn't initialise Python threading properly when an introspection-loaded # library used threading, but the Python program didn't (see # https://bugzilla.gnome.org/show_bug.cgi?id=710447). Therefore, we create a # dummy thread to force Python to initialise threading to accomodate these # broken pygobject versions. import threading threading.Thread(target=lambda: None).start() 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 _check_elem(self, elem): if elem is None: # that element couldn't be created, maybe because plugins are # missing? raise Exception(u"failed to construct pipeline (did you install " u"all necessary GStreamer plugins?)") else: return elem def _setup_pipeline(self): """Setup the pipeline.""" self.pipe = Gst.Pipeline() # elements self.src = self._check_elem(Gst.ElementFactory.make("filesrc", "src")) self.pipe.add(self.src) self.decbin = self._check_elem(Gst.ElementFactory.make("decodebin", "decbin")) self.pipe.add(self.decbin) self.conv = self._check_elem(Gst.ElementFactory.make("audioconvert", "conv")) self.pipe.add(self.conv) self.res = self._check_elem(Gst.ElementFactory.make("audioresample", "res")) self.pipe.add(self.res) self.rg = self._check_elem(Gst.ElementFactory.make("rganalysis", "rg")) self.pipe.add(self.rg) self.sink = self._check_elem(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 # By default, GLib (and therefore GStreamer) assume any filename to be # UTF-8 encoded, regardless of locale settings (though most Unix # systems, Linux at least, should be configured for UTF-8 anyways these # days). The file name we pass to GStreamer is encoded with the system # default encoding here: if that's UTF-8, everyone's happy, if it isn't, # GLib's UTF-8 assumption needs to be overridden using the # G_FILENAME_ENCODING environment variable (set to locale to tell GLib # that all file names passed to it are encoded in the system encoding). # That way, people on non-UTF-8 systems or with non-UTF-8 file names can # still force all file name processing into a different encoding. self.src.set_property("location", fname.encode(util.getfilesystemencoding())) self._current_file = fname self.emit("track-started", 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", 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.3.3/rgain/script/0000775000175000017500000000000012415577371014262 5ustar fkfk00000000000000rgain-1.3.3/rgain/script/collectiongain.py0000664000175000017500000003123612415574725017633 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2014 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 albumid, rgio from rgain.script import * from rgain.script.replaygain import do_gain CURRENT_CACHE_VERSION = 1 # 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 cache_entry_valid(filepath, record): return ( 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)) def read_cache(cache_file): if os.path.isfile(cache_file): try: with open(cache_file, "rb") as f: cache = pickle.load(f) if not isinstance(cache, tuple) or len(cache) != 2: print ou(u"Invalid cache, ignoring it") return {} cache_version = cache[0] if cache_version != CURRENT_CACHE_VERSION: print ou(u"Old cache format, ignoring it") return {} files = cache[1] if not isinstance(files, dict): print ou(u"Invalid cache, ignoring it") return {} to_remove = set() for filepath, record in files.iteritems(): if not cache_entry_valid(filepath, record): to_remove.add(filepath) for filepath in to_remove: # remove fishy entries del files[filepath] return files except Exception, exc: print ou(u"Error while reading the cache, continuing without it - " u"%s" % exc) return {} 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) with open(cache_file, "wb") as f: pickle.dump((CURRENT_CACHE_VERSION, files), f, 2) except Exception, exc: print ou(u"Error while writing the cache - %s" % exc) def collect_files(music_dir, files, visited_cache, is_supported_format): 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), getfilesystemencoding()) properpath = os.path.join(dirpath, filename) mtime = os.path.getmtime(properpath) # check the cache if filepath in visited_cache: visited_cache[filepath] = True record = files[filepath] if mtime <= record[1]: # the file's still ok continue ext = os.path.splitext(filename)[1] if is_supported_format(ext): i += 1 print ou(u" [%i] %s |" % (i, filepath)), try: tags = mutagen.File(os.path.join(music_dir, filepath)) if tags is None: raise Exception() album_id = albumid.get_album_id(tags) print ou(album_id or u"") # fields here: album_id, mtime, already_processed files[filepath] = (album_id, mtime, False) except: # TODO: Maybe optionally abort here? print ou(u"IGNORED: unreadable file or unsupported format") def transform_cache(files): # transform ``files`` into lists of things to process 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 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 BaseException, exc: # We can't reliably serialise and pass the exception information to the # driver process so we stringify it here. # And yes, we want to catch KeyboardInterrupt et al. 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, 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, if desired if not ignore_cache: files = read_cache(cache_file) else: files = {} print "Collecting files ..." # whenever this part is stopped (KeyboardInterrupt/other exception), the # cache is written to disk so all progress persists try: visited_cache = dict.fromkeys(files.iterkeys(), False) collect_files(music_dir, files, visited_cache, rgio.BaseFormatsMap(mp3_format).is_supported_format) # clean cache for filepath, visited in visited_cache.items(): if not visited: del visited_cache[filepath] del files[filepath] # hopefully gets rid of at least one huge data structure del visited_cache albums, single_tracks = transform_cache(files) # 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: write_cache(cache_file, files) print "All finished." def collectiongain_options(): opts = common_options() opts.add_option("--ignore-cache", help="Do not use the file cache at all.", dest="ignore_cache", action="store_true") opts.add_option("--regain", help="Fully reprocess everything. Same as " "'--force --ignore-cache'.", dest="regain", 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") if opts.regain: opts.force = opts.ignore_cache = True 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.3.3/rgain/script/replaygain.py0000664000175000017500000001652712415574725017002 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2014 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 * # 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, getfilesystemencoding()) for filename in files] formats_map = rgio.BaseFormatsMap(mp3_format) newfiles = [] for filename in files: if not formats_map.is_supported_format(os.path.splitext(filename)[1]): 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, 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.3.3/rgain/script/__init__.py0000664000175000017500000001116512415574725016377 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2014 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 from rgain import __version__ import rgain.rgio __all__ = [ "getfilesystemencoding", "ou", "un", "Error", "init_gstreamer", "common_options"] # we re-export this function for convenience and compatibility from rgain.util import getfilesystemencoding stdout_encoding = sys.stdout.encoding or getfilesystemencoding() def ou(arg): # turn arg into a string suitable for console output if isinstance(arg, str): # we aggressively suggest that anything passed into this function should # be a Unicode string by rejecting non-ASCII byte input. 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 %s" % __version__) 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.3.3/rgain/util.py0000664000175000017500000000250312415574725014305 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2014 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 sys def getfilesystemencoding(): # get file system encoding, making sure never to return None return sys.getfilesystemencoding() or sys.getdefaultencoding() @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.3.3/rgain/__init__.py0000664000175000017500000000375612415574725015102 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2014 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 .version import __version__ __all__ = ["__version__", "GainData"] 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 # any string from glib stuff should be a UTF-8 byte string, right? self.message = gerror.message.decode("utf-8") self.debug = debug.decode("utf-8") def __unicode__(self): return u"GST error: %s (%s)" % (self.message, self.debug) rgain-1.3.3/rgain/rgio.py0000664000175000017500000003232212415575122014262 0ustar fkfk00000000000000# -*- coding: utf-8 -*- # # Copyright (c) 2009-2014 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 os.path import warnings import mutagen from mutagen.easyid3 import EasyID3 from rgain import GainData class AudioFormatError(Exception): def __init__(self, filename): Exception.__init__(self, u"Did not understand file: %s" % filename) # some generic helper functions 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 # interface for ReplayGain reading/writing class class BaseTagReaderWriter(object): def read_gain(self, filename): raise NotImplementedError def write_gain(self, filename, track_gain, album_gain): raise NotImplementedError # class to read and write ReplayGain data from/to simple tags. The default tags # match the rg.org specification for Ogg (at least Vorbis), Flac and WavPack # files class SimpleTagReaderWriter(BaseTagReaderWriter): TRACK_GAIN_TAG = u"replaygain_track_gain" TRACK_PEAK_TAG = u"replaygain_track_peak" ALBUM_GAIN_TAG = u"replaygain_album_gain" ALBUM_PEAK_TAG = u"replaygain_album_peak" REF_LOUDNESS_TAGS = [u"replaygain_reference_loudness"] # default behaviour; override in a subclass if necessary, e.g. for MP3 def _get_tags_object(self, filename): return mutagen.File(filename) def read_gain(self, filename): tags = self._get_tags_object(filename) if tags is None: raise AudioFormatError(filename) track_gain = self._read_gain_data(tags, self.TRACK_GAIN_TAG, self.TRACK_PEAK_TAG) album_gain = self._read_gain_data(tags, self.ALBUM_GAIN_TAG, self.ALBUM_PEAK_TAG) ref_level = self._read_ref_loudness(tags) if ref_level is not None: if track_gain: track_gain.ref_level = ref_level if album_gain: album_gain.ref_level = ref_level return track_gain, album_gain def _read_gain_data(self, tags, gain_tag, peak_tag): 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 else: gaindata = None return gaindata def _read_ref_loudness(self, tags): for tag in self.REF_LOUDNESS_TAGS: if tag in tags: ref_level = parse_db(tags[tag][0]) if ref_level is not None: return ref_level return None def write_gain(self, filename, track_gain, album_gain): tags = self._get_tags_object(filename) if tags is None: raise AudioFormatError(filename) if track_gain: tags[self.TRACK_GAIN_TAG] = self._dump_gain(track_gain.gain) tags[self.TRACK_PEAK_TAG] = self._dump_peak(track_gain.peak) for tag in self.REF_LOUDNESS_TAGS: tags[tag] = self._dump_ref_level(track_gain.ref_level) if album_gain: tags[self.ALBUM_GAIN_TAG] = self._dump_gain(album_gain.gain) tags[self.ALBUM_PEAK_TAG] = self._dump_peak(album_gain.peak) tags.save() def _dump_gain(self, gain): return u"%.8f dB" % gain def _dump_peak(self, peak): return u"%.8f" % peak def _dump_ref_level(self, ref_level): return u"%i dB" % ref_level # MP4 support class MP4TagReaderWriter(SimpleTagReaderWriter): FORMAT = "----:com.apple.iTunes:replaygain_%s_%s" TRACK_GAIN_TAG = FORMAT % ("track", "gain") TRACK_PEAK_TAG = FORMAT % ("track", "peak") ALBUM_GAIN_TAG = FORMAT % ("album", "gain") ALBUM_PEAK_TAG = FORMAT % ("album", "peak") REF_LOUDNESS_TAGS = [] # Mutagen 1.22 has a bug (?) such that MP4 values cannot be unicode objects # so we encode everything to ASCII here # https://code.google.com/p/mutagen/issues/detail?id=164 def _dump_gain(self, gain): return SimpleTagReaderWriter._dump_gain(self, gain).encode("ascii") def _dump_peak(self, peak): return SimpleTagReaderWriter._dump_peak(self, peak).encode("ascii") def _dump_ref_level(self, ref_level): return SimpleTagReaderWriter._dump_ref_level( self, ref_level).encode("ascii") # MP3 support base class class MP3TagReaderWriter(SimpleTagReaderWriter): _EXTRA_TXXX_TAGS = [ u"replaygain_track_gain", u"replaygain_track_peak", u"replaygain_album_gain", u"replaygain_album_peak", u"replaygain_reference_loudness", u"QuodLibet::replaygain_reference_loudness", ] class _ReplaygainEasyID3(EasyID3): pass for key in _EXTRA_TXXX_TAGS: _ReplaygainEasyID3.RegisterTXXXKey(u"TXXX:%s" % key, key) def _get_tags_object(self, filename): return self._ReplaygainEasyID3(filename) # 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. class MP3rgorgTagReaderWriter(MP3TagReaderWriter): TRACK_GAIN_TAG = u"TXXX:replaygain_track_gain" TRACK_PEAK_TAG = u"TXXX:replaygain_track_peak" ALBUM_GAIN_TAG = u"TXXX:replaygain_album_gain" ALBUM_PEAK_TAG = u"TXXX:replaygain_album_peak" REF_LOUDNESS_TAGS = [u"TXXX:replaygain_reference_loudness"] # clamp RVA2 values to certain limits so that they do not overflow RVA2_GAIN_MIN = -64 RVA2_GAIN_MAX = float(64 * 512 - 1) / 512.0 RVA2_PEAK_MIN = 0 RVA2_PEAK_MAX = float(2 ** 16 - 1) / float(2 ** 15) def clamp(v, min, max): clamped = False if v < min: v = min clamped = True if v > max: v = max clamped = True return v, clamped def clamp_rva2_gain(v): v, clamped = clamp(v, RVA2_GAIN_MIN, RVA2_GAIN_MAX) 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): v, clamped = clamp(v, RVA2_PEAK_MIN, RVA2_PEAK_MAX) if clamped: warnings.warn("peak value was out of bounds for RVA2 frame and was " "clamped to %.5f" % v) return v def clamp_gain_data(gain_data): if gain_data is None: return None else: return GainData(clamp_rva2_gain(gain_data.gain), clamp_rva2_peak(gain_data.peak), gain_data.ref_level) # ID3v2 support for legacy RVA2-frames-based format according to # http://wiki.hydrogenaudio.org/index.php?title=ReplayGain_specification#ID3v2 class MP3RVA2TagReaderWriter(MP3TagReaderWriter): # EasyID3 maps these to RVA2 by default TRACK_GAIN_TAG = u"replaygain_track_gain" TRACK_PEAK_TAG = u"replaygain_track_peak" ALBUM_GAIN_TAG = u"replaygain_album_gain" ALBUM_PEAK_TAG = u"replaygain_album_peak" # since there's no proper reference loudness tag for the legacy format, we # use reasonably common TXXX tags; these were registered in the superclass REF_LOUDNESS_TAGS = [ u"TXXX:replaygain_reference_loudness", u"TXXX:QuodLibet::replaygain_reference_loudness", ] def _dump_gain(self, gain): return SimpleTagReaderWriter._dump_gain(self, clamp_rva2_gain(gain)) def _dump_peak(self, peak): return SimpleTagReaderWriter._dump_peak(self, clamp_rva2_peak(peak)) # 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 class MP3DefaultTagReaderWriter(BaseTagReaderWriter): def __init__(self, rgorg_readerwriter, rva2_readerwriter): self.rgorg = rgorg_readerwriter self.rva2 = rva2_readerwriter def read_gain(self, filename): rgorg_track_gain, rgorg_album_gain = self.rgorg.read_gain(filename) rva2_track_gain, rva2_album_gain = self.rva2.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_track_gain is None or rva2_track_gain is None: # ensure that track gain exists for both return (None, None) if (not gaindata_almost_equal(rgorg_track_gain, rva2_track_gain) or not gaindata_almost_equal(rgorg_album_gain, rva2_album_gain)): # The different formats are not similar enough. return (None, None) else: # the formats seem to match, we obviously use the non-clamped one return (rgorg_track_gain, rgorg_album_gain) def write_gain(self, filename, track_gain, album_gain): self.rgorg.write_gain(filename, track_gain, album_gain) self.rva2.write_gain(filename, track_gain, album_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)) # code to pull everything together class UnknownFiletype(Exception): pass class BaseFormatsMap(object): _simplereaderwriter = SimpleTagReaderWriter() _mp4readerwriter = MP4TagReaderWriter() _mp3_rgorg_readerwriter = MP3rgorgTagReaderWriter() _mp3_rva2_readerwriter = MP3RVA2TagReaderWriter() _mp3_default_readerwriter = MP3DefaultTagReaderWriter( _mp3_rgorg_readerwriter, _mp3_rva2_readerwriter) BASE_MAP = { ".ogg": _simplereaderwriter, ".oga": _simplereaderwriter, ".flac": _simplereaderwriter, ".wv": _simplereaderwriter, ".m4a": _mp4readerwriter, ".mp4": _mp4readerwriter, } MP3_FORMATS = { None: _mp3_default_readerwriter, "default": _mp3_default_readerwriter, "replaygain.org": _mp3_rgorg_readerwriter, "fb2k": _mp3_rgorg_readerwriter, "legacy": _mp3_rva2_readerwriter, "ql": _mp3_rva2_readerwriter, } 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) def is_supported_format(self, ext): ext_lower = ext.lower() return ext_lower in self.BASE_MAP or ext_lower in self.more_mappings def read_gain(self, filename): ext = os.path.splitext(filename)[1].lower() 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.read_gain(filename) def write_gain(self, filename, trackgain, albumgain): ext = os.path.splitext(filename)[1].lower() 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.write_gain(filename, trackgain, albumgain) rgain-1.3.3/man/0000775000175000017500000000000012415577371012431 5ustar fkfk00000000000000rgain-1.3.3/man/replaygain.rst0000664000175000017500000000755012415574725015325 0ustar fkfk00000000000000============ replaygain ============ -------------------------------- single file Replay Gain editor -------------------------------- :Date: DATE :Version: VERSION :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. By default, all given files be assumed to be part of a single album and album gain data will be calculated for them. 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.3.3/man/collectiongain.rst0000664000175000017500000001002312415574725016151 0ustar fkfk00000000000000================ collectiongain ================ ------------------------------------------ large scale Replay Gain calculating tool ------------------------------------------ :Date: DATE :Version: VERSION :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*. Files belonging to the same album will be identified using the file tags and album Replay Gain data will be calculated for them. 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 Do not use the file cache at all. --regain Fully reprocess everything. Same as ``--force --ignore-cache``. -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)**