mirror of
https://bitbucket.org/smil3y/kde-extraapps.git
synced 2025-02-24 19:02:53 +00:00
350 lines
14 KiB
C++
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;
|
|
}
|
|
|