/* * StreamEncoder.cpp * ----------------- * Purpose: Exporting streamed music files. * Notes : none * Authors: Joern Heusipp * OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #include "stdafx.h" #include "StreamEncoder.h" #include "StreamEncoderMP3.h" #include "Mptrack.h" #include "../soundlib/Sndfile.h" #include "../common/misc_util.h" #include "../common/mptStringBuffer.h" #ifdef MPT_WITH_LAME #if defined(MPT_BUILD_MSVC) #include #else #include #endif #endif // MPT_WITH_LAME OPENMPT_NAMESPACE_BEGIN /////////////////////////////////////////////////////////////////////////////////////////////////// // ID3v2.4 Tags struct ID3v2Header { uint8 signature[3]; uint8 version[2]; uint8be flags; uint32be size; }; MPT_BINARY_STRUCT(ID3v2Header, 10) struct ID3v2Frame { char frameid[4]; uint32be size; uint16be flags; }; MPT_BINARY_STRUCT(ID3v2Frame, 10) // charset... choose text ending accordingly. // $00 = ISO-8859-1. Terminated with $00. // $01 = UTF-16 with BOM. Terminated with $00 00. // $02 = UTF-16BE without BOM. Terminated with $00 00. // $03 = UTF-8. Terminated with $00. #define ID3v2_CHARSET '\3' #define ID3v2_TEXTENDING '\0' struct ReplayGain { enum GainTag { TagSkip, TagReserve, TagWrite }; GainTag Tag; float TrackPeak; bool TrackPeakValid; float TrackGaindB; bool TrackGaindBValid; ReplayGain() : Tag(TagSkip) , TrackPeak(0.0f) , TrackPeakValid(false) , TrackGaindB(0.0f) , TrackGaindBValid(false) { return; } }; class ID3V2Tagger { private: Encoder::StreamSettings settings; public: // Write Tags void WriteID3v2Tags(std::ostream &s, const FileTags &tags, ReplayGain replayGain = ReplayGain()); ID3V2Tagger(const Encoder::StreamSettings &settings_); private: // Convert Integer to Synchsafe Integer (see ID3v2.4 specs) uint32 intToSynchsafe(uint32 in); // Return maximum value that fits into a syncsafe int uint32 GetMaxSynchsafeInt() const; // Write a frame void WriteID3v2Frame(const char cFrameID[4], std::string sFramecontent, std::ostream &s); // Return an upper bound for the size of all replay gain frames uint32 GetMaxReplayGainFramesSizes(); uint32 GetMaxReplayGainTxxxTrackGainFrameSize(); uint32 GetMaxReplayGainTxxxTrackPeakFrameSize(); // Write out all ReplayGain frames void WriteID3v2ReplayGainFrames(ReplayGain replaygain, std::ostream &s); // Size of our tag uint32 totalID3v2Size; }; /////////////////////////////////////////////////// // CFileTagging - helper class for writing tags ID3V2Tagger::ID3V2Tagger(const Encoder::StreamSettings &settings_) : settings(settings_) , totalID3v2Size(0) { return; } /////////////////////////////////////////////////////////////////////////////////////////////////// // ID3v2.4 Tags // Convert Integer to Synchsafe Integer (see ID3v2.4 specs) // Basically, it's a BigEndian integer, but the MSB of all bytes is 0. // Thus, a 32-bit integer turns into a 28-bit integer. uint32 ID3V2Tagger::intToSynchsafe(uint32 in) { uint32 out = 0, steps = 0; do { out |= (in & 0x7F) << steps; steps += 8; } while(in >>= 7); return out; } // Return maximum value that fits into a syncsafe int uint32 ID3V2Tagger::GetMaxSynchsafeInt() const { return 0x0fffffffu; } // Write Tags void ID3V2Tagger::WriteID3v2Tags(std::ostream &s, const FileTags &tags, ReplayGain replayGain) { if(!s) return; ID3v2Header tHeader; std::streampos fOffset = s.tellp(); uint32 paddingSize = 0; totalID3v2Size = 0; // Correct header will be written later (tag size missing) memcpy(tHeader.signature, "ID3", 3); tHeader.version[0] = 0x04; // Version 2.4.0 tHeader.version[1] = 0x00; // Ditto tHeader.flags = 0; // No flags tHeader.size = 0; // will be filled later s.write(reinterpret_cast(&tHeader), sizeof(tHeader)); totalID3v2Size += sizeof(tHeader); WriteID3v2Frame("TIT2", mpt::ToCharset(mpt::Charset::UTF8, tags.title), s); WriteID3v2Frame("TPE1", mpt::ToCharset(mpt::Charset::UTF8, tags.artist), s); WriteID3v2Frame("TCOM", mpt::ToCharset(mpt::Charset::UTF8, tags.artist), s); WriteID3v2Frame("TALB", mpt::ToCharset(mpt::Charset::UTF8, tags.album), s); WriteID3v2Frame("TCON", mpt::ToCharset(mpt::Charset::UTF8, tags.genre), s); //WriteID3v2Frame("TYER", mpt::ToCharset(mpt::Charset::UTF8, tags.year), s); // Deprecated WriteID3v2Frame("TDRC", mpt::ToCharset(mpt::Charset::UTF8, tags.year), s); WriteID3v2Frame("TBPM", mpt::ToCharset(mpt::Charset::UTF8, tags.bpm), s); WriteID3v2Frame("WXXX", mpt::ToCharset(mpt::Charset::UTF8, tags.url), s); WriteID3v2Frame("TENC", mpt::ToCharset(mpt::Charset::UTF8, tags.encoder), s); WriteID3v2Frame("COMM", mpt::ToCharset(mpt::Charset::UTF8, tags.comments), s); if(replayGain.Tag == ReplayGain::TagReserve) { paddingSize += GetMaxReplayGainFramesSizes(); } else if(replayGain.Tag == ReplayGain::TagWrite) { std::streampos replayGainBeg = s.tellp(); WriteID3v2ReplayGainFrames(replayGain, s); std::streampos replayGainEnd = s.tellp(); paddingSize += GetMaxReplayGainFramesSizes() - static_cast(replayGainEnd - replayGainBeg); } // Write Padding uint32 totalID3v2SizeWithoutPadding = totalID3v2Size; paddingSize += settings.MP3ID3v2MinPadding; totalID3v2Size += paddingSize; if(settings.MP3ID3v2PaddingAlignHint > 0) { totalID3v2Size = mpt::align_up(totalID3v2Size, settings.MP3ID3v2PaddingAlignHint); paddingSize = totalID3v2Size - totalID3v2SizeWithoutPadding; } for(size_t i = 0; i < paddingSize; i++) { char c = 0; s.write(&c, 1); } // Write correct header (update tag size) tHeader.size = intToSynchsafe(totalID3v2Size - sizeof(tHeader)); s.seekp(fOffset); s.write(reinterpret_cast(&tHeader), sizeof(tHeader)); s.seekp(totalID3v2Size - sizeof(tHeader), std::ios::cur); } uint32 ID3V2Tagger::GetMaxReplayGainTxxxTrackGainFrameSize() { return mpt::saturate_cast(sizeof(ID3v2Frame) + 1 + std::strlen("REPLAYGAIN_TRACK_GAIN") + 1 + std::strlen("-123.45 dB") + 1); // should be enough } uint32 ID3V2Tagger::GetMaxReplayGainTxxxTrackPeakFrameSize() { return mpt::saturate_cast(sizeof(ID3v2Frame) + 1 + std::strlen("REPLAYGAIN_TRACK_PEAK") + 1 + std::strlen("2147483648.123456") + 1); // unrealistic worst case } uint32 ID3V2Tagger::GetMaxReplayGainFramesSizes() { uint32 size = 0; if(settings.MP3ID3v2WriteReplayGainTXXX) { size += GetMaxReplayGainTxxxTrackGainFrameSize(); size += GetMaxReplayGainTxxxTrackPeakFrameSize(); } return size; } void ID3V2Tagger::WriteID3v2ReplayGainFrames(ReplayGain replayGain, std::ostream &s) { if(settings.MP3ID3v2WriteReplayGainTXXX && replayGain.TrackGaindBValid) { std::string content; content += std::string(1, 0x00); // ISO-8859-1 content += std::string("REPLAYGAIN_TRACK_GAIN"); content += std::string(1, '\0'); int32 gainTimes100 = mpt::saturate_round(replayGain.TrackGaindB * 100.0f); if(gainTimes100 < 0) { content += "-"; gainTimes100 = std::abs(gainTimes100); } content += mpt::afmt::dec(gainTimes100 / 100); content += "."; content += mpt::afmt::dec0<2>(gainTimes100 % 100); content += " "; content += "dB"; content += std::string(1, '\0'); if(sizeof(ID3v2Frame) + content.size() <= GetMaxReplayGainTxxxTrackGainFrameSize()) { ID3v2Frame frame; std::memset(&frame, 0, sizeof(ID3v2Frame)); std::memcpy(&frame.frameid, "TXXX", 4); frame.size = intToSynchsafe(static_cast(content.size())); frame.flags = 0x4000; // discard if audio data changed s.write(reinterpret_cast(&frame), sizeof(ID3v2Frame)); s.write(content.data(), content.size()); } } if(settings.MP3ID3v2WriteReplayGainTXXX && replayGain.TrackPeakValid) { std::string content; content += std::string(1, 0x00); // ISO-8859-1 content += std::string("REPLAYGAIN_TRACK_PEAK"); content += std::string(1, '\0'); int32 peakTimes1000000 = mpt::saturate_round(std::fabs(replayGain.TrackPeak) * 1000000.0f); std::string number; number += mpt::afmt::dec(peakTimes1000000 / 1000000); number += "."; number += mpt::afmt::dec0<6>(peakTimes1000000 % 1000000); content += number; content += std::string(1, '\0'); if(sizeof(ID3v2Frame) + content.size() <= GetMaxReplayGainTxxxTrackPeakFrameSize()) { ID3v2Frame frame; std::memset(&frame, 0, sizeof(ID3v2Frame)); std::memcpy(&frame.frameid, "TXXX", 4); frame.size = intToSynchsafe(static_cast(content.size())); frame.flags = 0x4000; // discard if audio data changed s.write(reinterpret_cast(&frame), sizeof(ID3v2Frame)); s.write(content.data(), content.size()); } } } // Write a ID3v2 frame void ID3V2Tagger::WriteID3v2Frame(const char cFrameID[4], std::string sFramecontent, std::ostream &s) { if(!cFrameID[0] || sFramecontent.empty() || !s) return; if(!memcmp(cFrameID, "COMM", 4)) { // English language for comments - no description following (hence the text ending nullchar(s)) // For language IDs, see https://en.wikipedia.org/wiki/ISO-639-2 sFramecontent = "eng" + (ID3v2_TEXTENDING + sFramecontent); } if(!memcmp(cFrameID, "WXXX", 4)) { // User-defined URL field (we have no description for the URL, so we leave it out) sFramecontent = ID3v2_TEXTENDING + sFramecontent; } sFramecontent = ID3v2_CHARSET + sFramecontent; sFramecontent += ID3v2_TEXTENDING; if(sFramecontent.size() <= GetMaxSynchsafeInt()) { ID3v2Frame tFrame; std::memset(&tFrame, 0, sizeof(ID3v2Frame)); std::memcpy(&tFrame.frameid, cFrameID, 4); // ID tFrame.size = intToSynchsafe(static_cast(sFramecontent.size())); // Text size tFrame.flags = 0x0000; // No flags s.write(reinterpret_cast(&tFrame), sizeof(tFrame)); s.write(sFramecontent.c_str(), sFramecontent.size()); totalID3v2Size += static_cast((sizeof(tFrame) + sFramecontent.size())); } } #ifdef MPT_WITH_LAME using lame_t = lame_global_flags *; static void GenreEnumCallback(int num, const char *name, void *cookie) { MPT_UNREFERENCED_PARAMETER(num); Encoder::Traits &traits = *reinterpret_cast(cookie); if(name) { traits.genres.push_back(mpt::ToUnicode(mpt::Charset::ISO8859_1, name)); } } static Encoder::Traits BuildTraits(bool compatible) { Encoder::Traits traits; traits.fileExtension = P_("mp3"); traits.fileShortDescription = (compatible ? U_("Compatible MP3") : U_("MP3")); traits.encoderSettingsName = (compatible ? U_("MP3LameCompatible") : U_("MP3Lame")); traits.fileDescription = (compatible ? U_("MPEG-1 Layer 3") : U_("MPEG-1/2 Layer 3")); traits.canTags = true; traits.genres.clear(); id3tag_genre_list(&GenreEnumCallback, &traits); traits.modesWithFixedGenres = (compatible ? Encoder::ModeCBR : Encoder::ModeInvalid); traits.maxChannels = 2; traits.samplerates = (compatible ? mpt::make_vector(mpeg1layer3_samplerates) : mpt::make_vector(layer3_samplerates) ); traits.modes = (compatible ? Encoder::ModeCBR : (Encoder::ModeABR | Encoder::ModeQuality)); traits.bitrates = (compatible ? mpt::make_vector(mpeg1layer3_bitrates) : mpt::make_vector(layer3_bitrates) ); traits.defaultSamplerate = 44100; traits.defaultChannels = 2; traits.defaultMode = (compatible ? Encoder::ModeCBR : Encoder::ModeQuality); traits.defaultBitrate = 256; traits.defaultQuality = 0.8f; return traits; } class MP3LameStreamWriter : public StreamWriterBase { private: bool compatible; Encoder::Settings settings; Encoder::Mode Mode; bool gfp_inited; lame_t gfp; enum ID3Type { ID3None, ID3v1, ID3v2Lame, ID3v2OpenMPT, }; ID3Type id3type; std::streamoff id3v2Size; FileTags Tags; public: MP3LameStreamWriter(std::ostream &stream, bool compatible, const Encoder::Settings &settings_, const FileTags &tags) : StreamWriterBase(stream) , compatible(compatible) , settings(settings_) { Mode = Encoder::ModeInvalid; gfp_inited = false; gfp = lame_t(); id3type = ID3v2Lame; id3v2Size = 0; if(!gfp) { gfp = lame_init(); } uint32 samplerate = settings.Samplerate; uint16 channels = settings.Channels; if(settings.Tags) { if(compatible) { id3type = ID3v1; } else if(settings.Details.MP3LameID3v2UseLame) { id3type = ID3v2Lame; } else { id3type = ID3v2OpenMPT; } } else { id3type = ID3None; } id3v2Size = 0; lame_set_in_samplerate(gfp, samplerate); lame_set_num_channels(gfp, channels); int lameQuality = settings.Details.MP3LameQuality; lame_set_quality(gfp, lameQuality); if(settings.Mode == Encoder::ModeCBR) { if(compatible) { if(settings.Bitrate >= 32) { // For maximum compatibility, // force samplerate to a samplerate supported by MPEG1 streams. if(samplerate <= 32000) { samplerate = 32000; } else if(samplerate >= 48000) { samplerate = 48000; } else { samplerate = 44100; } lame_set_out_samplerate(gfp, samplerate); } else { // A very low bitrate was chosen, // force samplerate to lowest possible for MPEG2. // Disable unofficial MPEG2.5 however. lame_set_out_samplerate(gfp, 16000); } } lame_set_brate(gfp, settings.Bitrate); lame_set_VBR(gfp, vbr_off); if(compatible) { lame_set_bWriteVbrTag(gfp, 0); lame_set_strict_ISO(gfp, 1); lame_set_disable_reservoir(gfp, 1); } else { lame_set_bWriteVbrTag(gfp, 1); } } else if(settings.Mode == Encoder::ModeABR) { lame_set_brate(gfp, settings.Bitrate); lame_set_VBR(gfp, vbr_abr); lame_set_bWriteVbrTag(gfp, 1); } else { float lame_quality = 10.0f - (settings.Quality * 10.0f); Limit(lame_quality, 0.0f, 9.999f); lame_set_VBR_quality(gfp, lame_quality); lame_set_VBR(gfp, vbr_default); lame_set_bWriteVbrTag(gfp, 1); } lame_set_decode_on_the_fly(gfp, settings.Details.MP3LameCalculatePeakSample ? 1 : 0); // see LAME docs for why lame_set_findReplayGain(gfp, settings.Details.MP3LameCalculateReplayGain ? 1 : 0); switch(id3type) { case ID3None: lame_set_write_id3tag_automatic(gfp, 0); break; case ID3v1: id3tag_init(gfp); id3tag_v1_only(gfp); break; case ID3v2Lame: id3tag_init(gfp); id3tag_add_v2(gfp); id3tag_v2_only(gfp); id3tag_set_pad(gfp, settings.Details.MP3ID3v2MinPadding); break; case ID3v2OpenMPT: lame_set_write_id3tag_automatic(gfp, 0); break; } Mode = settings.Mode; if(settings.Tags) { if(id3type == ID3v2Lame || id3type == ID3v1) { // Lame API expects Latin1, which is sad, but we cannot change that. if(!tags.title.empty()) id3tag_set_title( gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.title ).c_str()); if(!tags.artist.empty()) id3tag_set_artist( gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.artist ).c_str()); if(!tags.album.empty()) id3tag_set_album( gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.album ).c_str()); if(!tags.year.empty()) id3tag_set_year( gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.year ).c_str()); if(!tags.comments.empty()) id3tag_set_comment(gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.comments).c_str()); if(!tags.trackno.empty()) id3tag_set_track( gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.trackno ).c_str()); if(!tags.genre.empty()) id3tag_set_genre( gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.genre ).c_str()); } else if(id3type == ID3v2OpenMPT) { Tags = tags; std::streampos id3beg = f.tellp(); ID3V2Tagger tagger(settings.Details); ReplayGain replayGain; if(settings.Details.MP3LameCalculatePeakSample || settings.Details.MP3LameCalculateReplayGain) { replayGain.Tag = ReplayGain::TagReserve; } tagger.WriteID3v2Tags(f, tags, replayGain); std::streampos id3end = f.tellp(); id3v2Size = id3end - id3beg; } } } void WriteInterleaved(size_t count, const float *interleaved) override { if(!gfp_inited) { lame_init_params(gfp); gfp_inited = true; } const int count_max = 0xffff; while(count > 0) { int count_chunk = std::clamp(mpt::saturate_cast(count), int(0), count_max); buf.resize(count_chunk + (count_chunk+3)/4 + 7200); int result = 0; if(lame_get_num_channels(gfp) == 1) { // lame always assumes stereo input with interleaved interface, so use non-interleaved for mono result = lame_encode_buffer_ieee_float(gfp, interleaved, nullptr, count_chunk, mpt::byte_cast(buf.data()), mpt::saturate_cast(buf.size())); } else { result = lame_encode_buffer_interleaved_ieee_float(gfp, interleaved, count_chunk, mpt::byte_cast(buf.data()), mpt::saturate_cast(buf.size())); } buf.resize((result >= 0) ? result : 0); if(result == -2) { throw std::bad_alloc(); } WriteBuffer(); count -= static_cast(count_chunk); } } void WriteFinalize() override { if(!gfp_inited) { lame_init_params(gfp); gfp_inited = true; } buf.resize(7200); buf.resize(lame_encode_flush(gfp, mpt::byte_cast(buf.data()), mpt::saturate_cast(buf.size()))); WriteBuffer(); ReplayGain replayGain; if(settings.Details.MP3LameCalculatePeakSample) { replayGain.TrackPeak = std::fabs(lame_get_PeakSample(gfp)) / 32768.0f; replayGain.TrackPeakValid = true; } if(settings.Details.MP3LameCalculateReplayGain) { replayGain.TrackGaindB = lame_get_RadioGain(gfp) / 10.0f; replayGain.TrackGaindBValid = true; } if(id3type == ID3v2OpenMPT && (settings.Details.MP3LameCalculatePeakSample || settings.Details.MP3LameCalculateReplayGain)) { // update ID3v2 tag with replay gain information replayGain.Tag = ReplayGain::TagWrite; std::streampos endPos = f.tellp(); f.seekp(fStart); std::string tagdata(static_cast(id3v2Size), '\0'); f.write(tagdata.data(), id3v2Size); // clear out the old tag f.seekp(fStart); ID3V2Tagger tagger(settings.Details); tagger.WriteID3v2Tags(f, Tags, replayGain); f.seekp(endPos); } if(id3type == ID3v2Lame) { id3v2Size = lame_get_id3v2_tag(gfp, nullptr, 0); } else if(id3type == ID3v2OpenMPT) { // id3v2Size already set } if(!compatible) { std::streampos endPos = f.tellp(); f.seekp(fStart + id3v2Size); buf.resize(lame_get_lametag_frame(gfp, nullptr, 0)); buf.resize(lame_get_lametag_frame(gfp, (unsigned char*)buf.data(), buf.size())); WriteBuffer(); f.seekp(endPos); } } virtual ~MP3LameStreamWriter() { if(!gfp) { return; } lame_close(gfp); gfp = lame_t(); gfp_inited = false; } }; #endif // MPT_WITH_LAME MP3Encoder::MP3Encoder(MP3EncoderType type) : m_Type(type) { #ifdef MPT_WITH_LAME if(type == MP3EncoderLame) { m_Type = MP3EncoderLame; SetTraits(BuildTraits(false)); return; } if(type == MP3EncoderLameCompatible) { m_Type = MP3EncoderLameCompatible; SetTraits(BuildTraits(true)); return; } #endif // MPT_WITH_LAME } bool MP3Encoder::IsAvailable() const { return false #ifdef MPT_WITH_LAME || (m_Type == MP3EncoderLame) || (m_Type == MP3EncoderLameCompatible) #endif // MPT_WITH_LAME ; } std::unique_ptr MP3Encoder::ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const { std::unique_ptr result = nullptr; if(false) { // nothing #ifdef MPT_WITH_LAME } else if(m_Type == MP3EncoderLame || m_Type == MP3EncoderLameCompatible) { result = std::make_unique(file, (m_Type == MP3EncoderLameCompatible), settings, tags); #endif // MPT_WITH_LAME } return result; } mpt::ustring MP3Encoder::DescribeQuality(float quality) const { #ifdef MPT_WITH_LAME if(m_Type == MP3EncoderLame) { static constexpr int q_table[11] = { 240, 220, 190, 170, 160, 130, 120, 100, 80, 70, 50 }; // http://wiki.hydrogenaud.io/index.php?title=LAME int q = mpt::saturate_round((1.0f - quality) * 10.0f); if(q < 0) q = 0; if(q >= 10) { return MPT_UFORMAT("VBR -V{} (~{} kbit)")(U_("9.999"), q_table[q]); } else { return MPT_UFORMAT("VBR -V{} (~{} kbit)")(q, q_table[q]); } } #endif // MPT_WITH_LAME return EncoderFactoryBase::DescribeQuality(quality); } mpt::ustring MP3Encoder::DescribeBitrateABR(int bitrate) const { return EncoderFactoryBase::DescribeBitrateABR(bitrate); } OPENMPT_NAMESPACE_END