/**************************************************************************************** * Copyright (c) 2009 Alex Merry * * * * 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 . * ****************************************************************************************/ // NOTE: this file is used by amarokcollectionscanner and CANNOT use any amaroklib // code [this includes debug()] #include "MetaReplayGain.h" #include #include // Taglib #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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( *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( 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( fileref.file() ) ) { if ( file->tag() ) map = readXiphTags( file->tag() ); } else if ( TagLib::FLAC::File *file = dynamic_cast( 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( fileref.file() ) ) { if ( file->tag() ) map = readXiphTags( file->tag() ); } else if ( TagLib::WavPack::File *file = dynamic_cast( fileref.file() ) ) { if ( file->APETag() ) map = readAPETags( file->APETag() ); } else if ( TagLib::TrueAudio::File *file = dynamic_cast( fileref.file() ) ) { if ( file->ID3v2Tag() ) map = readID3v2Tags( file->ID3v2Tag() ); } else if ( TagLib::Ogg::Speex::File *file = dynamic_cast( fileref.file() ) ) { if ( file->tag() ) map = readXiphTags( file->tag() ); } else if ( TagLib::MPC::File *file = dynamic_cast( 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( 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( fileref.file() ) ) { if ( file->tag() ) map = readMP4Tags( file->getMP4Tag() ); } #endif return map; }