Commits

Felix Krull committed aede64d

Initial checkin (not that it's not more or less done ...)

  • Participants
  • Tags 1.0

Comments (0)

Files changed (12)

+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  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
+them 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 prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  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.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey 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;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU 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 that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  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.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+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.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     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
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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 3 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, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program 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, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU 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.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+include COPYING
+include MANIFEST.in
+Introduction
+------------
+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 [http://replaygain.org/] 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.
+
+Requirements
+------------
+ - Python 2.4 (probably) [http://python.org/]
+ - Mutagen [http://code.google.com/p/mutagen/]
+ - GStreamer 0.10 [http://gstreamer.org/]
+ - pygst 0.10 [http://gstreamer.freedesktop.org/modules/gst-python.html]
+
+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 (with 3 different formats)
+Basic usage is simple:
+ $ replaygain AUDIOFILE1 AUDIOFILE2 ...
+There are some options; see 'em 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 2 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 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 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
+-----------
+In contrast to modern audio file formats, there is no commonly accepted standard
+for Replay Gain information in MP3 files. There is the ``mp3gain`` program that
+directly changes the audio data, but this means you have to decide on one form
+of Replay Gain: Track gain or album gain. Plus, it's ugly. And it might kill
+your cat. Or make your plant eat your dog.
+Because there are several not-quite-standards to store Replay Gain data in MP3
+files, both ``replaygain`` and ``collectiongain`` force you to make a choice
+(well, they don't *force* you technically, but don't let that fool you) through
+the '--mp3-format' option. They support 3 formats:
+ - 'ql'
+   Replay Gain information is stored in ID3v2.4 RVA2 frames. Quod Libet uses
+   this format, so choose this if you run QL. This is also the default setting.
+ - 'fb2k'
+   Replay Gain information is stored in ID3v2 TXXX frames. Foobar2000 reads and
+   writes this format; Quod Libet reads it, but converts it to 'ql' format
+   without asking.
+ - 'mp3gain'
+   Replay Gain information is stored in APE v2 tags. ``mp3gain`` writes these
+   tags (this doesn't mean this format works like the ``mp3gain`` program; it
+   doesn't, see above). Maybe there are programs that support this format. If
+   you know, you could tell me.
+I hope that's enough confusion for now.
+

File rgain/__init__.py

+# -*- coding: utf-8 -*-
+# 
+# Copyright (c) 2009 Felix Krull <f_krull@gmx.de>
+# 
+# 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 3, 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))
+

File rgain/rgcalc.py

+# -*- coding: utf-8 -*-
+# 
+# Copyright (c) 2009 Felix Krull <f_krull@gmx.de>
+# 
+# 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 3, 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 pygst
+pygst.require("0.10")
+import gst
+import gobject
+
+from rgain import GainData
+
+GST_TAG_REFERENCE_LEVEL = "replaygain-reference-level"
+
+
+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
+       * wether to force a re-calculation, even if Replay Gain data is
+         discovered (defaults to ``False``),
+       * the reference loudness level to use (defaults to 89 dB).
+     - connect to the signals the class provides (using the GObject signal
+       system) to get notified when everything is done:
+       * ``all-finished`` is emitted when all tracks were analyzed (has no
+         arguments),
+       * ``track-finished`` is emitted when one track is done (has the file name
+         as single argument)
+     - 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
+     - 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). ``track_data`` may not
+    contain all file names you passed, if ``force`` is False - it just isn't
+    computed. 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, ()),
+        "track-started": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                          (gobject.TYPE_PYOBJECT,)),
+        "track-finished": (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)
+    
+    
+    # private functions
+    def _setup_pipeline(self):
+        """Setup the pipeline."""
+        self.pipe = gst.Pipeline("replaygain")
+        
+        # elements
+        self.decbin = gst.element_factory_make("decodebin", "decbin")
+        self.pipe.add(self.decbin)
+        self.conv = gst.element_factory_make("audioconvert", "conv")
+        self.pipe.add(self.conv)
+        self.res = gst.element_factory_make("audioresample", "res")
+        self.pipe.add(self.res)
+        self.rg = gst.element_factory_make("rganalysis", "rg")
+        self.pipe.add(self.rg)
+        self.sink = gst.element_factory_make("fakesink", "sink")
+        self.pipe.add(self.sink)
+        
+        # link
+        gst.element_link_many(self.conv, self.res, self.rg, 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):
+        self.rg.set_property("forced", self.force)
+        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.
+        """
+        # only when there already is a source
+        if hasattr(self, "src"):
+            self.src.unlink(self.decbin)
+            self.pipe.remove(self.src)
+        
+        # set the next file
+        try:
+            fname = self._files_iter.next()
+        except StopIteration:
+            self.emit("all-finished")
+            return False
+        
+        # make a new src element
+        self.src = gst.element_factory_make("filesrc", "src")
+        self.src.set_property("location", fname.encode("utf-8"))
+        
+        self._current_file = fname
+        
+        self.pipe.add(self.src)
+        self.src.link(self.decbin)
+        
+        self.rg.set_property("num-tracks", 1)
+        
+        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))
+        
+        # process just every tag
+        for tag in tags.keys():
+            if tag == gst.TAG_TRACK_GAIN:
+                trackdata.gain = tags[tag]
+            elif tag == gst.TAG_TRACK_PEAK:
+                trackdata.peak = tags[tag]
+            elif tag == GST_TAG_REFERENCE_LEVEL:
+                trackdata.ref_level = tags[tag]
+            
+            elif tag == gst.TAG_ALBUM_GAIN:
+                self.album_data.gain = tags[tag]
+            elif tag == gst.TAG_ALBUM_PEAK:
+                self.album_data.peak = tags[tag]
+    
+    
+    # event handlers
+    def _on_pad_added(self, decbin, new_pad):
+        try:
+            decbin.link(self.conv)
+        except gst.LinkError:
+            # this one didn't work. Hopefully the next try's better
+            pass
+    
+    def _on_pad_removed(self, decbin, old_pad):
+        decbin.unlink(self.conv)
+    
+    def _on_message(self, bus, msg):
+        if msg.type == gst.MESSAGE_TAG:
+            if not msg.src == self.rg:
+                return
+            self._process_tags(msg)
+        elif msg.type == gst.MESSAGE_EOS:
+            self.emit("track-finished", self._current_file)
+            self.rg.set_locked_state(True)
+            self.pipe.set_state(gst.STATE_NULL)
+            ret = self._next_file()
+            if ret:
+                #self.emit("track-started", self._current_file)
+                self.rg.set_locked_state(False)
+                self.pipe.set_state(gst.STATE_PLAYING)
+
+
+
+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.
+    """
+    def on_finished(src):
+        # all done
+        loop.quit()
+    
+    rg = ReplayGain(*args, **kwargs)
+    rg.connect("all-finished", on_finished)
+    loop = gobject.MainLoop()
+    rg.start()
+    loop.run()
+    return (rg.track_data, rg.album_data)
+

File rgain/rgio.py

+# -*- coding: utf-8 -*-
+# 
+# Copyright (c) 2009 Felix Krull <f_krull@gmx.de>
+# 
+# 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 3, 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 mutagen
+from mutagen.id3 import ID3, RVA2, TXXX
+from mutagen.apev2 import APEv2File
+
+from rgain import GainData
+
+
+
+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
+
+# 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)
+    
+    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 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()
+
+
+# ID3 for Quod Libet
+def mp3_ql_read_gain(filename):
+    tags = ID3(filename)
+    
+    def read_gain_data(desc):
+        tag = u"RVA2:%s" % desc
+        if tag in tags:
+            frame = tags[tag]
+            gaindata = GainData(frame.gain, frame.peak)
+            if u"TXXX:QuodLibet::replaygain_reference_loudness" in tags:
+                ref_level = parse_db(tags[
+                    u"TXXX:QuodLibet::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_ql_write_gain(filename, trackdata, albumdata):
+    tags = ID3(filename)
+    
+    if trackdata:
+        trackgain = RVA2(desc=u"track", channel=1, gain=trackdata.gain,
+                         peak=trackdata.peak)
+        tags.add(trackgain)
+        # write QL reference loudness
+        reflevel = TXXX(encoding=3,
+                        desc=u"QuodLibet::replaygain_reference_loudness",
+                        text=[u"%i dB" % trackdata.ref_level])
+        tags.add(reflevel)
+    if albumdata:
+        albumgain = RVA2(desc=u"album", channel=1, gain=albumdata.gain,
+                         peak=albumdata.peak)
+        tags.add(albumgain)
+    
+    tags.save()
+
+
+# ID3 for foobar2000
+def mp3_fb2k_read_gain(filename):
+    tags = ID3(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_fb2k_write_gain(filename, trackdata, albumdata):
+    tags = ID3(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()
+
+
+# APE v2 tags as written by mp3gain
+# I hope this works
+def mp3_mp3gain_read_gain(filename):
+    tags = APEv2File(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 mp3_mp3gain_write_gain(filename, trackdata, albumdata):
+    tags = APEv2File(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()
+
+
+# rudimentary MP3 all-in-one support
+def create_mp3_checks(ql=False, fb2k=False, mp3gain=False):
+    def mp3_check_gain(filename):
+        trackdata = None
+        albumdata = None
+        if mp3gain:
+            trackdata, albumdata = mp3_mp3gain_read_gain(filename)
+            if trackdata is None or albumdata is None:
+                return trackdata, albumdata
+        if fb2k:
+            trackdata, albumdata = mp3_fb2k_read_gain(filename)
+            if trackdata is None or albumdata is None:
+                return trackdata, albumdata
+        if ql:
+            trackdata, albumdata = mp3_ql_read_gain(filename)
+            if trackdata is None or albumdata is None:
+                return trackdata, albumdata
+        return trackdata, albumdata
+    
+    def mp3_write_gain(filename, trackdata, albumdata):
+        if mp3gain:
+            mp3_mp3gain_write_gain(filename, trackdata, albumdata)
+        if fb2k:
+            mp3_fb2k_write_gain(filename, trackdata, albumdata)
+        if ql:
+            mp3_ql_write_gain(filename, trackdata, albumdata)
+    
+    return mp3_check_gain, mp3_write_gain
+
+
+
+# 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),
+    }
+    
+    def __init__(self, mp3_format, more_mappings=None):
+        # yeah, you need to choose
+        self.more_mappings = more_mappings if more_mappings else {}
+        if mp3_format == "ql":
+            self.more_mappings[".mp3"] = (mp3_ql_read_gain, mp3_ql_write_gain)
+        elif mp3_format == "fb2k":
+            self.more_mappings[".mp3"] = (mp3_fb2k_read_gain,
+                                          mp3_fb2k_write_gain)
+        elif mp3_format == "mp3gain":
+            self.more_mappings[".mp3"] = (mp3_mp3gain_read_gain,
+                                          mp3_mp3gain_write_gain)
+        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)
+

File rgain/script/__init__.py

+# -*- coding: utf-8 -*-
+# 
+# Copyright (c) 2009 Felix Krull <f_krull@gmx.de>
+# 
+# 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 3, 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
+
+
+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):
+    pass
+
+
+def common_options():
+    opts = OptionParser(version="%prog 1.0")
+    
+    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. Since there is no commonly accepted "
+                    "standard for Replay Gain in MP3 files, you need to "
+                    "choose. Possible formats are 'ql' (used by Quod Libet), "
+                    "'fb2k' (read and written by foobar2000, also understood "
+                    "by Quod Libet) and 'mp3gain' (tags as written by the "
+                    "'mp3gain' program; this doesn't modify the MP3 audio "
+                    "data as said program does). Default is '%default'.",
+                    dest="mp3_format", action="store", type="choice",
+                    choices=["ql", "fb2k", "mp3gain"])
+    
+    opts.set_defaults(force=False, dry_run=False, ref_level=89, mp3_format="ql")
+    
+    return opts
+

File rgain/script/collectiongain.py

+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2009 Felix Krull <f_krull@gmx.de>
+#
+# 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 3, 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
+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
+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: error - %s" % (filepath, exc))
+    
+    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"<single track>")
+                # 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)
+
+
+def do_gain_all(music_dir, albums, single_tracks, files, ref_level=89,
+              force=False, dry_run=False, mp3_format="ql", stop_on_error=False):
+    if single_tracks:
+        do_gain((os.path.join(music_dir, path) for path in single_tracks),
+                ref_level, force, dry_run, False, mp3_format)
+        # update cache information
+        if not dry_run:
+            update_cache(files, music_dir, single_tracks, None)
+        print
+    
+    for album_id, album_files in albums.iteritems():
+        print ou(u"%s:" % album_id),
+        do_gain((os.path.join(music_dir, path) for path in album_files),
+                ref_level, force, dry_run, True, mp3_format)
+        # update cache
+        if not dry_run:
+            update_cache(files, music_dir, album_files, album_id)
+        print
+
+
+def do_collectiongain(music_dir, ref_level=89, force=False, dry_run=False,
+                      mp3_format="ql", ignore_cache=False):
+    music_dir = un(music_dir, sys.getfilesystemencoding())
+    
+    music_abspath = os.path.abspath(music_dir)
+    musicpath_hash = md5(music_abspath).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)
+        print
+        
+        # gain everything that has survived the cleansing
+        do_gain_all(music_dir, albums, single_tracks, files, ref_level, force,
+                  dry_run, mp3_format)
+    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.set_defaults(ignore_cache=False)
+    
+    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():
+    optparser = collectiongain_options()
+    opts, args = optparser.parse_args()
+    if len(args) != 1:
+        optparser.error("pass one directory path")
+    
+    try:
+        do_collectiongain(args[0], opts.ref_level, opts.force, opts.dry_run,
+                          opts.mp3_format, opts.ignore_cache)
+    except Error, exc:
+        print >> sys.stderr, ou(unicode(exc))
+        sys.exit(1)
+    except KeyboardInterrupt:
+        print "Interrupted."
+
+
+if __name__ == "__main__":
+    collectiongain()
+

File rgain/script/replaygain.py

+# -*- coding: utf-8 -*-
+# 
+# Copyright (c) 2009 Felix Krull <f_krull@gmx.de>
+# 
+# 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 3, 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
+
+import gobject
+
+from rgain import rgio
+from rgain.script import ou, un, Error, common_options
+
+
+# calculate the gain for the given files
+def calculate_gain(files, ref_level):
+    # this has to be done here since Gstreamer hooks into the command line
+    # arguments if it's imported on module level
+    from rgain import rgcalc
+    
+    # handlers
+    def on_finished(src):
+        loop.quit()
+    
+    def on_trk_started(src, filename):
+        print ou("  %s:" % filename),
+        sys.stdout.flush()
+        
+    def on_trk_finished(src, filename):
+        gaindata = rg.track_data.get(filename)
+        if gaindata:
+            print "%.2f dB" % gaindata.gain
+        else:
+            print "done"
+    
+    rg = rgcalc.ReplayGain(files, True, ref_level)
+    rg.connect("all-finished", on_finished)
+    rg.connect("track-started", on_trk_started)
+    rg.connect("track-finished", on_trk_finished)
+    loop = gobject.MainLoop()
+    rg.start()
+    loop.run()
+    return rg.track_data, rg.album_data
+
+
+def do_gain(files, ref_level=89, force=False, dry_run=False, album=True,
+            mp3_format="ql"):
+    
+    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: error - %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: error - %s" % (filename, exc))
+            else:
+                print "done"
+    
+    print "Done"
+
+
+# a simple Replay Gain dump
+def show_rgain_info(filenames, mp3_format="ql"):
+    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 "  <Error reading Replay Gain: %r>" % exc
+            continue
+        
+        if not trackdata and not albumdata:
+            print "  <No Replay Gain information>"
+        
+        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 rgain():
+    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 >> sys.stderr, ou(unicode(exc))
+            sys.exit(1)
+        except KeyboardInterrupt:
+            print "Interrupted."
+
+
+if __name__ == "__main__":
+    rgain()
+

File scripts/collectiongain

+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2009 Felix Krull <f_krull@gmx.de>
+
+from rgain.script.collectiongain import collectiongain
+
+if __name__ == "__main__":
+    collectiongain()
+

File scripts/replaygain

+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2009 Felix Krull <f_krull@gmx.de>
+
+from rgain.script.replaygain import rgain
+
+if __name__ == "__main__":
+    rgain()
+
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2009 Felix Krull <f_krull@gmx.de>
+
+from distutils.core import setup
+
+setup(
+    name="rgain",
+    version="1.0",
+    description="Multi-format Replay Gain utilities",
+    author="Felix Krull",
+    author_email="f_krull@gmx.de",
+    
+    packages=["rgain", "rgain.script"],
+    scripts=["scripts/replaygain", "scripts/collectiongain"],
+    
+    requires=["pygst", "mutagen"],
+)
+