Files
tenacity/modules/import-export/mod-mpg123/ImportMP3_MPG123.cpp
Avery King dc4467d2bc Replace codebase with modified Audacity 3.7.1
Some of our basic changes remained, but a lot of work still needs to be
done. At least it builds though!

I have a few notes off the top of my head writing this:

- Our build system was kept and modified to work with the new codebase.
  Similarly, the codebase was modified to work with our build system.
- We don't support Audacity's full wxBase restrictions, so I just
  replaced it with wxWidgets::wxWidgets for now. (See above).
- There are still networking features, which are to be removed in the
  next commit.
- pffft was added as a library in lib-src, and I wrote a new
  CMakeLists.txt for it.
- I modified WrapAllegro.h to use our allegro.h shim instead.

Signed-off-by: Avery King <avery98@pm.me>
2025-01-04 14:54:13 -08:00

533 lines
14 KiB
C++

/* SPDX-License-Identifier: GPL-2.0-or-later */
/*!********************************************************************
Audacity: A Digital Audio Editor
ImportMP3_MPG123.cpp
Dmitry Vedenko
*/
#include <wx/defs.h>
#include <cstddef>
#include <cstring>
#include "Import.h"
#include "BasicUI.h"
#include "ImportPlugin.h"
#include "ImportUtils.h"
#include "ImportProgressListener.h"
#include "Project.h"
#define DESC XO("MP3 files")
#include <wx/file.h>
#include <wx/string.h>
#include <wx/log.h>
#include <mpg123.h>
#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<ImportFileHandle> 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<LibFileFormats::AcidizerTags>& 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<ImportFileHandle> MP3ImportPlugin::Open(
const FilePath &Filename, AudacityProject *)
{
auto handle = std::make_unique<MP3ImportFileHandle>(Filename);
if (!handle->Open())
return nullptr;
return handle;
}
static Importer::RegisteredImportPlugin registered
{
"MP3",
std::make_unique<MP3ImportPlugin>()
};
// ============================================================================
// 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<LibFileFormats::AcidizerTags>&)
{
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<float> conversionBuffer;
int ret = MPG123_OK;
while ((ret = mpg123_decode_frame(mHandle, &frameIndex, &data, &dataSize)) ==
MPG123_OK)
{
if(framesCount > 0)
progressListener.OnImportProgress(static_cast<double>(frameIndex) / static_cast<double>(framesCount));
if(IsCancelled())
{
progressListener.OnImportResult(ImportProgressListener::ImportResult::Cancelled);
return;
}
//VS: doesn't implement Stop behavior...
constSamplePtr samples = reinterpret_cast<constSamplePtr>(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<float>(
reinterpret_cast<const double*>(data)[sampleIndex]);
}
samples = reinterpret_cast<constSamplePtr>(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<MP3ImportFileHandle*>(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<MP3ImportFileHandle*>(handle)->mFile.Seek(
offset, GetWXSeekMode(whence));
}
} // namespace