kde-extraapps/amarok/shared/MetaReplayGain.cpp
2015-01-31 00:30:50 +00:00

350 lines
14 KiB
C++

/****************************************************************************************
* Copyright (c) 2009 Alex Merry <alex.merry@kdemail.net> *
* *
* 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, see <http://www.gnu.org/licenses/>. *
****************************************************************************************/
// NOTE: this file is used by amarokcollectionscanner and CANNOT use any amaroklib
// code [this includes debug()]
#include "MetaReplayGain.h"
#include <QString>
#include <cmath>
// Taglib
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnon-virtual-dtor"
#include <tag.h>
#include <tlist.h>
#include <tstring.h>
#include <textidentificationframe.h>
#include <apetag.h>
#include <fileref.h>
#include <flacfile.h>
#include <id3v2tag.h>
#include <mpcfile.h>
#include <mpegfile.h>
#include <oggfile.h>
#include <oggflacfile.h>
#include <speexfile.h>
#include <trueaudiofile.h>
#include <vorbisfile.h>
#include <wavpackfile.h>
#include <asffile.h>
#include <mp4file.h>
#pragma GCC diagnostic pop
// converts a peak value from the normal digital scale form to the more useful decibel form
// decibels are relative to the /adjusted/ waveform
static qreal peakToDecibels( qreal scaleVal )
{
if ( scaleVal > 0 )
return 20 * log10( scaleVal );
else
return 0;
}
// NOTE: representation is taken to be a binary value with units in the first column,
// 1/2 in the second and so on.
static qreal readRVA2PeakValue( const TagLib::ByteVector &data, int bits, bool *ok )
{
qreal peak = 0.0;
// discarding digits at the end reduces precision, but doesn't otherwise change the value
if ( bits > 32 )
bits = 32;
// the +7 makes sure we round up when we divide by 8
unsigned int bytes = (bits + 7) / 8;
// normalize appears not to write a peak at all, and hence sets bits to 0
if ( bits == 0 )
{
if ( ok )
*ok = true;
}
else if ( bits >= 4 && data.size() >= bytes ) // fewer than 4 bits would just be daft
{
// excessBits is the number of bits we have to discard at the end
unsigned int excessBits = (8 * bytes) - bits;
// mask has 1s everywhere but the last /excessBits/ bits
quint32 mask = 0xffffffff << excessBits;
quint32 rawValue = 0;
for ( unsigned int i = 0; i < bytes; ++i )
{
rawValue <<= 8;
rawValue += (unsigned char)data[i];
}
rawValue &= mask;
peak = rawValue;
// amount we need to "shift" the value right to make the first digit the unit column
unsigned int rightShift = (8 * bytes) - 1;
peak /= (qreal)(1 << rightShift);
if ( ok )
*ok = true;
}
else
{
if ( ok )
*ok = false;
}
return peak;
}
// adds the converted version of the scale value if it is a valid, non-negative float
static void maybeAddPeak( const TagLib::String &scaleVal, Meta::ReplayGainTag key, Meta::ReplayGainTagMap *map )
{
// scale value is >= 0, and typically not much bigger than 1
QString value = TStringToQString( scaleVal );
bool ok = false;
qreal peak = value.toFloat( &ok );
if ( ok && peak >= 0 )
(*map)[key] = peakToDecibels( peak );
}
static void maybeAddGain( const TagLib::String &input, Meta::ReplayGainTag key, Meta::ReplayGainTagMap *map )
{
QString value = TStringToQString( input ).remove( " dB" );
bool ok = false;
qreal gain = value.toFloat( &ok );
if (ok)
(*map)[key] = gain;
}
static Meta::ReplayGainTagMap readID3v2Tags( TagLib::ID3v2::Tag *tag )
{
Meta::ReplayGainTagMap map;
{ // ID3v2.4.0 native replay gain tag support (as written by Quod Libet, for example).
TagLib::ID3v2::FrameList frames = tag->frameListMap()["RVA2"];
frames.append(tag->frameListMap()["XRVA"]);
if ( !frames.isEmpty() )
{
for ( unsigned int i = 0; i < frames.size(); ++i )
{
// we have to parse this frame ourselves
// ID3v2 frame header is 10 bytes, so skip that
TagLib::ByteVector data = frames[i]->render().mid( 10 );
unsigned int offset = 0;
QString desc( data.data() );
offset += desc.count() + 1;
unsigned int channel = data.mid( offset, 1 ).toUInt( true );
// channel 1 is the main volume - the only one we care about
if ( channel == 1 )
{
++offset;
qint16 adjustment512 = data.mid( offset, 2 ).toShort( true );
qreal adjustment = ( (qreal)adjustment512 ) / 512.0;
offset += 2;
unsigned int peakBits = data.mid( offset, 1 ).toUInt( true );
++offset;
bool ok = false;
qreal peak = readRVA2PeakValue( data.mid( offset ), peakBits, &ok );
if ( ok )
{
if ( desc.toLower() == "album" )
{
map[Meta::ReplayGain_Album_Gain] = adjustment;
map[Meta::ReplayGain_Album_Peak] = peakToDecibels( peak );
}
else if ( desc.toLower() == "track" || !map.contains( Meta::ReplayGain_Track_Gain ) )
{
map[Meta::ReplayGain_Track_Gain] = adjustment;
map[Meta::ReplayGain_Track_Peak] = peakToDecibels( peak );
}
}
}
}
if ( !map.isEmpty() )
return map;
}
}
{ // Foobar2000-style ID3v2.3.0 tags
TagLib::ID3v2::FrameList frames = tag->frameListMap()["TXXX"];
for ( TagLib::ID3v2::FrameList::Iterator it = frames.begin(); it != frames.end(); ++it ) {
TagLib::ID3v2::UserTextIdentificationFrame* frame =
dynamic_cast<TagLib::ID3v2::UserTextIdentificationFrame*>( *it );
if ( frame && frame->fieldList().size() >= 2 )
{
QString desc = TStringToQString( frame->description() ).toLower();
if ( desc == "replaygain_album_gain" )
maybeAddGain( frame->fieldList()[1], Meta::ReplayGain_Album_Gain, &map );
if ( desc == "replaygain_album_peak" )
maybeAddPeak( frame->fieldList()[1], Meta::ReplayGain_Album_Peak, &map );
if ( desc == "replaygain_track_gain" )
maybeAddGain( frame->fieldList()[1], Meta::ReplayGain_Track_Gain, &map );
if ( desc == "replaygain_track_peak" )
maybeAddPeak( frame->fieldList()[1], Meta::ReplayGain_Track_Peak, &map );
}
}
}
return map;
}
static Meta::ReplayGainTagMap readAPETags( TagLib::APE::Tag *tag )
{
Meta::ReplayGainTagMap map;
const TagLib::APE::ItemListMap &items = tag->itemListMap();
if ( items.contains("REPLAYGAIN_TRACK_GAIN") )
{
maybeAddGain( items["REPLAYGAIN_TRACK_GAIN"].values()[0], Meta::ReplayGain_Track_Gain, &map );
if ( items.contains("REPLAYGAIN_TRACK_PEAK") )
maybeAddPeak( items["REPLAYGAIN_TRACK_PEAK"].values()[0], Meta::ReplayGain_Track_Peak, &map );
}
if ( items.contains("REPLAYGAIN_ALBUM_GAIN") )
{
maybeAddGain( items["REPLAYGAIN_ALBUM_GAIN"].values()[0], Meta::ReplayGain_Album_Gain, &map );
if ( items.contains("REPLAYGAIN_ALBUM_PEAK") )
maybeAddPeak( items["REPLAYGAIN_ALBUM_PEAK"].values()[0], Meta::ReplayGain_Album_Peak, &map );
}
return map;
}
static Meta::ReplayGainTagMap readXiphTags( TagLib::Ogg::XiphComment *tag )
{
const TagLib::Ogg::FieldListMap &tagMap = tag->fieldListMap();
Meta::ReplayGainTagMap outputMap;
if ( !tagMap["REPLAYGAIN_TRACK_GAIN"].isEmpty() )
{
maybeAddGain( tagMap["REPLAYGAIN_TRACK_GAIN"].front(), Meta::ReplayGain_Track_Gain, &outputMap );
if ( !tagMap["REPLAYGAIN_TRACK_PEAK"].isEmpty() )
maybeAddPeak( tagMap["REPLAYGAIN_TRACK_PEAK"].front(), Meta::ReplayGain_Track_Peak, &outputMap );
}
if ( !tagMap["REPLAYGAIN_ALBUM_GAIN"].isEmpty() )
{
maybeAddGain( tagMap["REPLAYGAIN_ALBUM_GAIN"].front(), Meta::ReplayGain_Album_Gain, &outputMap );
if ( !tagMap["REPLAYGAIN_ALBUM_PEAK"].isEmpty() )
maybeAddPeak( tagMap["REPLAYGAIN_ALBUM_PEAK"].front(), Meta::ReplayGain_Album_Peak, &outputMap );
}
return outputMap;
}
static Meta::ReplayGainTagMap readASFTags( TagLib::ASF::Tag *tag )
{
const TagLib::ASF::AttributeListMap &tagMap = tag->attributeListMap();
Meta::ReplayGainTagMap outputMap;
if ( !tagMap["REPLAYGAIN_TRACK_GAIN"].isEmpty() )
{
maybeAddGain( tagMap["REPLAYGAIN_TRACK_GAIN"].front().toString(), Meta::ReplayGain_Track_Gain, &outputMap );
if ( !tagMap["REPLAYGAIN_TRACK_PEAK"].isEmpty() )
maybeAddPeak( tagMap["REPLAYGAIN_TRACK_PEAK"].front().toString(), Meta::ReplayGain_Track_Peak, &outputMap );
}
if ( !tagMap["REPLAYGAIN_ALBUM_GAIN"].isEmpty() )
{
maybeAddGain( tagMap["REPLAYGAIN_ALBUM_GAIN"].front().toString(), Meta::ReplayGain_Album_Gain, &outputMap );
if ( !tagMap["REPLAYGAIN_ALBUM_PEAK"].isEmpty() )
maybeAddPeak( tagMap["REPLAYGAIN_ALBUM_PEAK"].front().toString(), Meta::ReplayGain_Album_Peak, &outputMap );
}
return outputMap;
}
// Bad news: ReplayGain in MP4 is not actually standardized in any way. Maybe reimplement at some point...maybe. See
// http://www.hydrogenaudio.org/forums/lofiversion/index.php/t14322.html
#ifdef DO_NOT_USE_THIS_UNTIL_FIXED
static Meta::ReplayGainTagMap readMP4Tags( TagLib::MP4::Tag *tag )
{
Meta::ReplayGainTagMap outputMap;
if ( !tag->trackReplayGain().isNull() ) {
maybeAddGain( tag->trackReplayGain(), Meta::ReplayGain_Track_Gain, &outputMap );
if ( !tag->trackReplayGainPeak().isNull() )
maybeAddPeak( tag->trackReplayGainPeak(), Meta::ReplayGain_Track_Peak, &outputMap );
}
if ( !tag->albumReplayGain().isNull() ) {
maybeAddGain( tag->albumReplayGain(), Meta::ReplayGain_Album_Gain, &outputMap );
if ( !tag->albumReplayGainPeak().isNull() )
maybeAddPeak( tag->albumReplayGainPeak(), Meta::ReplayGain_Album_Peak, &outputMap );
}
return outputMap;
}
#endif
Meta::ReplayGainTagMap
Meta::readReplayGainTags( const TagLib::FileRef &fileref )
{
Meta::ReplayGainTagMap map;
// NB: we can't get replay gain info from MPC files, since it's stored in some magic place
// and not in the APE tags, and taglib doesn't let us access the information (unless
// we want to parse the file ourselves).
// FIXME: should we try getting the info from the MPC APE tag just in case?
if ( TagLib::MPEG::File *file = dynamic_cast<TagLib::MPEG::File *>( fileref.file() ) )
{
if ( file->ID3v2Tag() )
map = readID3v2Tags( file->ID3v2Tag() );
if ( map.isEmpty() && file->APETag() )
map = readAPETags( file->APETag() );
}
else if ( TagLib::Ogg::Vorbis::File *file = dynamic_cast<TagLib::Ogg::Vorbis::File *>( fileref.file() ) )
{
if ( file->tag() )
map = readXiphTags( file->tag() );
}
else if ( TagLib::FLAC::File *file = dynamic_cast<TagLib::FLAC::File *>( fileref.file() ) )
{
if ( file->xiphComment() )
map = readXiphTags( file->xiphComment() );
if ( map.isEmpty() && file->ID3v2Tag() )
map = readID3v2Tags( file->ID3v2Tag() );
}
else if ( TagLib::Ogg::FLAC::File *file = dynamic_cast<TagLib::Ogg::FLAC::File *>( fileref.file() ) )
{
if ( file->tag() )
map = readXiphTags( file->tag() );
}
else if ( TagLib::WavPack::File *file = dynamic_cast<TagLib::WavPack::File *>( fileref.file() ) )
{
if ( file->APETag() )
map = readAPETags( file->APETag() );
}
else if ( TagLib::TrueAudio::File *file = dynamic_cast<TagLib::TrueAudio::File *>( fileref.file() ) )
{
if ( file->ID3v2Tag() )
map = readID3v2Tags( file->ID3v2Tag() );
}
else if ( TagLib::Ogg::Speex::File *file = dynamic_cast<TagLib::Ogg::Speex::File *>( fileref.file() ) )
{
if ( file->tag() )
map = readXiphTags( file->tag() );
}
else if ( TagLib::MPC::File *file = dynamic_cast<TagLib::MPC::File *>( fileref.file() ) )
{
// This is NOT the correct way to get replay gain tags from MPC files, but
// taglib doesn't allow us access to the real information.
// This allows people to work around this issue by copying their replay gain
// information to the APE tag.
if ( file->APETag() )
map = readAPETags( file->APETag() );
}
else if ( TagLib::ASF::File *file = dynamic_cast<TagLib::ASF::File *>( fileref.file() ) )
{
if ( file->tag() )
map = readASFTags( file->tag() );
}
// See comment above
#ifdef DO_NOT_USE_THIS_UNTIL_FIXED
else if ( TagLib::MP4::File *file = dynamic_cast<TagLib::MP4::File *>( fileref.file() ) )
{
if ( file->tag() )
map = readMP4Tags( file->getMP4Tag() );
}
#endif
return map;
}