/* SPDX-License-Identifier: GPL-2.0-or-later */ /*!******************************************************************** Audacity: A Digital Audio Editor ImportMP3_MPG123.cpp Dmitry Vedenko */ #include #include #include #include "Import.h" #include "BasicUI.h" #include "ImportPlugin.h" #include "ImportUtils.h" #include "ImportProgressListener.h" #include "Project.h" #define DESC XO("MP3 files") #include #include #include #include #include "Tags.h" #include "WaveTrack.h" #include "CodeConversions.h" #include "FromChars.h" namespace { const auto exts = { wxT("mp3"), wxT("mp2"), wxT("mpa") }; // ID2V2 genre can be quite complex: // (from https://id3.org/id3v2.3.0) // References to the ID3v1 genres can be made by, as first byte, enter // "(" followed by a number from the genres list (appendix A) and ended // with a ")" character. This is optionally followed by a refinement, // e.g. "(21)" or "(4)Eurodisco". Several references can be made in the // same frame, e.g. "(51)(39)". However, Audacity only supports one // genre, so we just skip ( a parse the number afterwards. wxString GetId3v2Genre(Tags& tags, const char* genre) { if (genre == nullptr) return {}; // It was observed, however, that Genre can use a different format if (genre[0] != '(') // We consider the string to be a genre name return audacity::ToWXString(genre); auto it = genre; auto end = it + std::strlen(it); while (*it == '(') { int tagValue; auto result = FromChars(++it, end, tagValue); // Parsing failed, consider it to be the genre if (result.ec != std::errc {}) break; const auto parsedGenre = tags.GetGenre(tagValue); if (!parsedGenre.empty()) return parsedGenre; it = result.ptr; // Nothing left to parse if (it == end) break; // Unexpected symbol in the tag if (*it != ')') break; ++it; } if (it != end) return audacity::ToWXString(it); return audacity::ToWXString(genre); } class MP3ImportPlugin final : public ImportPlugin { public: MP3ImportPlugin() : ImportPlugin( FileExtensions(exts.begin(), exts.end())) { #if MPG123_API_VERSION < 46 // Newer versions of the library don't need that anymore, but it is safe // to have the no-op call present for compatibility with old versions. mpg123_init(); #endif } wxString GetPluginStringID() override { return wxT("libmpg123"); } TranslatableString GetPluginFormatDescription() override { return DESC; } std::unique_ptr Open(const FilePath &Filename, AudacityProject*) override; }; // class MP3ImportPlugin class MP3ImportFileHandle final : public ImportFileHandleEx { public: MP3ImportFileHandle(const FilePath &filename); ~MP3ImportFileHandle(); TranslatableString GetFileDescription() override; ByteCount GetFileUncompressedBytes() override; void Import( ImportProgressListener& progressListener, WaveTrackFactory* trackFactory, TrackHolders& outTracks, Tags* tags, std::optional& outAcidTags) override; bool SetupOutputFormat(); void ReadTags(Tags* tags); wxInt32 GetStreamCount() override; const TranslatableStrings &GetStreamInfo() override; void SetStreamUsage(wxInt32 StreamID, bool Use) override; private: bool Open(); private: static ptrdiff_t ReadCallback(void* handle, void* buffer, size_t size); static off_t SeekCallback(void* handle, off_t offset, int whence); wxFile mFile; wxFileOffset mFileLen { 0 }; WaveTrackFactory* mTrackFactory { nullptr }; WaveTrack::Holder mTrack; unsigned mNumChannels { 0 }; mpg123_handle* mHandle { nullptr }; bool mFloat64Output {}; friend MP3ImportPlugin; }; // class MP3ImportFileHandle std::unique_ptr MP3ImportPlugin::Open( const FilePath &Filename, AudacityProject *) { auto handle = std::make_unique(Filename); if (!handle->Open()) return nullptr; return handle; } static Importer::RegisteredImportPlugin registered { "MP3", std::make_unique() }; // ============================================================================ // MP3ImportFileHandle // ============================================================================ MP3ImportFileHandle::MP3ImportFileHandle(const FilePath& filename) : ImportFileHandleEx(filename) { int errorCode = MPG123_OK; mHandle = mpg123_new(nullptr, &errorCode); if (errorCode != MPG123_OK) { wxLogError( "Failed to create MPG123 handle: %s", mpg123_plain_strerror(errorCode)); mHandle = nullptr; return; } errorCode = mpg123_replace_reader_handle( mHandle, ReadCallback, SeekCallback, nullptr); if (errorCode != MPG123_OK) { wxLogError( "Failed to set reader on the MPG123 handle: %s", mpg123_plain_strerror(errorCode)); mpg123_delete(mHandle); mHandle = nullptr; } // We force mpg123 to decode into floats mpg123_param(mHandle, MPG123_FLAGS, MPG123_GAPLESS | MPG123_FORCE_FLOAT, 0.0); if (errorCode != MPG123_OK) { wxLogError( "Failed to set options on the MPG123 handle", mpg123_plain_strerror(errorCode)); mpg123_delete(mHandle); mHandle = nullptr; } } MP3ImportFileHandle::~MP3ImportFileHandle() { // nullptr is a valid input for the mpg123_delete mpg123_delete(mHandle); } TranslatableString MP3ImportFileHandle::GetFileDescription() { return DESC; } auto MP3ImportFileHandle::GetFileUncompressedBytes() -> ByteCount { // We have to parse the file first using mpg123_scan, // we do not want to do that before the import starts. return 0; } wxInt32 MP3ImportFileHandle::GetStreamCount() { return 1; } const TranslatableStrings &MP3ImportFileHandle::GetStreamInfo() { static TranslatableStrings empty; return empty; } void MP3ImportFileHandle::SetStreamUsage(wxInt32 WXUNUSED(StreamID), bool WXUNUSED(Use)) { } void MP3ImportFileHandle::Import( ImportProgressListener& progressListener, WaveTrackFactory* trackFactory, TrackHolders& outTracks, Tags* tags, std::optional&) { BeginImport(); auto finalAction = finally([handle = mHandle]() { mpg123_close(handle); }); outTracks.clear(); mTrackFactory = trackFactory; long long framesCount = mpg123_framelength(mHandle); if (!SetupOutputFormat()) { progressListener.OnImportResult(ImportProgressListener::ImportResult::Error); return; } off_t frameIndex { 0 }; unsigned char* data { nullptr }; size_t dataSize { 0 }; std::vector conversionBuffer; int ret = MPG123_OK; while ((ret = mpg123_decode_frame(mHandle, &frameIndex, &data, &dataSize)) == MPG123_OK) { if(framesCount > 0) progressListener.OnImportProgress(static_cast(frameIndex) / static_cast(framesCount)); if(IsCancelled()) { progressListener.OnImportResult(ImportProgressListener::ImportResult::Cancelled); return; } //VS: doesn't implement Stop behavior... constSamplePtr samples = reinterpret_cast(data); const size_t samplesCount = dataSize / sizeof(float) / mNumChannels; // libmpg123 picks up the format based on some "internal" precision. // This case is not expected to happen if (mFloat64Output) { conversionBuffer.resize(samplesCount * mNumChannels); for (size_t sampleIndex = 0; sampleIndex < conversionBuffer.size(); ++sampleIndex) { conversionBuffer[sampleIndex] = static_cast( reinterpret_cast(data)[sampleIndex]); } samples = reinterpret_cast(conversionBuffer.data()); } // Just copy the interleaved data to the channels unsigned chn = 0; ImportUtils::ForEachChannel(*mTrack, [&](auto& channel) { channel.AppendBuffer( samples + sizeof(float) * chn, floatSample, samplesCount, mNumChannels, floatSample); ++chn; }); } if (ret != MPG123_DONE) { wxLogError( "Failed to decode MP3 file: %s", mpg123_plain_strerror(ret)); progressListener.OnImportResult(ImportProgressListener::ImportResult::Error); return; } ImportUtils::FinalizeImport(outTracks, *mTrack); ReadTags(tags); progressListener.OnImportResult(ImportProgressListener::ImportResult::Success); } bool MP3ImportFileHandle::SetupOutputFormat() { long rate; int channels; int encoding = MPG123_ENC_FLOAT_32; mpg123_getformat(mHandle, &rate, &channels, &encoding); mNumChannels = channels == MPG123_MONO ? 1 : 2; if (encoding != MPG123_ENC_FLOAT_32 && encoding != MPG123_ENC_FLOAT_64) { wxLogError("MPG123 returned unexpected encoding"); return false; } mFloat64Output = encoding == MPG123_ENC_FLOAT_64; mTrack = ImportUtils::NewWaveTrack( *mTrackFactory, mNumChannels, floatSample, rate); return true; } void MP3ImportFileHandle::ReadTags(Tags* tags) { mpg123_id3v1* v1; mpg123_id3v2* v2; int meta; meta = mpg123_meta_check(mHandle); if (meta & MPG123_ID3 && mpg123_id3(mHandle, &v1, &v2) == MPG123_OK) { if (v2 != nullptr && v2->title != nullptr && v2->title->fill > 0) tags->SetTag(TAG_TITLE, audacity::ToWXString(v2->title->p)); else if (v1 != nullptr && v1->title[0] != '\0') tags->SetTag(TAG_TITLE, audacity::ToWXString(v1->title)); if (v2 != nullptr && v2->artist != nullptr && v2->artist->fill > 0) tags->SetTag(TAG_ARTIST, audacity::ToWXString(v2->artist->p)); else if (v1 != nullptr && v1->artist[0] != '\0') tags->SetTag(TAG_ARTIST, audacity::ToWXString(v1->artist)); if (v2 != nullptr && v2->album != nullptr && v2->album->fill > 0) tags->SetTag(TAG_ALBUM, audacity::ToWXString(v2->album->p)); else if (v1 != nullptr && v1->album[0] != '\0') tags->SetTag(TAG_ALBUM, audacity::ToWXString(v1->album)); if (v2 != nullptr && v2->year != nullptr && v2->year->fill > 0) tags->SetTag(TAG_YEAR, audacity::ToWXString(v2->year->p)); else if (v1 != nullptr && v1->year[0] != '\0') tags->SetTag(TAG_YEAR, audacity::ToWXString(std::string(v1->year, 4))); if (v2 != nullptr && v2->genre != nullptr && v2->genre->fill > 0) tags->SetTag(TAG_GENRE, GetId3v2Genre(*tags, v2->genre->p)); else if (v1 != nullptr) tags->SetTag(TAG_GENRE, tags->GetGenre(v1->genre)); if (v2 != nullptr && v2->comment != nullptr && v2->comment->fill > 0) tags->SetTag(TAG_COMMENTS, audacity::ToWXString(v2->comment->p)); else if (v1 != nullptr && v1->comment[0] != '\0') tags->SetTag(TAG_COMMENTS, audacity::ToWXString(v1->comment)); if (v2 != nullptr) { for (size_t i = 0; i < v2->comments; ++i) { if (v2->comment_list[i].text.fill == 0) continue; tags->SetTag( audacity::ToWXString(std::string(v2->comment_list[i].id, 4)), audacity::ToWXString(v2->comment_list[i].text.p)); } for (size_t i = 0; i < v2->extras; ++i) { if (v2->extra[i].text.fill == 0) continue; tags->SetTag( audacity::ToWXString(std::string(v2->extra[i].id, 4)), audacity::ToWXString(v2->extra[i].text.p)); } // libmpg123 does not parse TRCK tag, we have to do it ourselves for (size_t i = 0; i < v2->texts; ++i) { if (memcmp(v2->text[i].id, "TRCK", 4) == 0) { tags->SetTag( TAG_TRACK, audacity::ToWXString(v2->text[i].text.p)); } } } } } bool MP3ImportFileHandle::Open() { if (mHandle == nullptr) return false; // Open the file if (!mFile.Open(GetFilename())) { return false; } // Get the length of the file mFileLen = mFile.Seek(0, wxFromEnd); if (mFileLen == wxInvalidOffset || mFile.Error()) { mFile.Close(); return false; } if (mFile.Seek(0, wxFromStart) == wxInvalidOffset || mFile.Error()) { mFile.Close(); return false; } // Check if file is an MP3 auto errorCode = mpg123_open_handle(mHandle, this); if (errorCode != MPG123_OK) return false; // Scan the file errorCode = mpg123_scan(mHandle); if (errorCode != MPG123_OK) return false; // Read the output format errorCode = mpg123_decode_frame(mHandle, nullptr, nullptr, nullptr); // First decode should read the format if (errorCode != MPG123_NEW_FORMAT) return false; return true; } ptrdiff_t MP3ImportFileHandle::ReadCallback( void* handle, void* buffer, size_t size) { return static_cast(handle)->mFile.Read(buffer, size); } wxSeekMode GetWXSeekMode(int whence) { switch (whence) { case SEEK_SET: return wxFromStart; case SEEK_CUR: return wxFromCurrent; case SEEK_END: return wxFromEnd; default: // We have covered all the lseek whence modes defined by POSIX // so this branch should not be reachable assert(false); return wxFromStart; } } off_t MP3ImportFileHandle::SeekCallback( void* handle, off_t offset, int whence) { return static_cast(handle)->mFile.Seek( offset, GetWXSeekMode(whence)); } } // namespace