1106 lines
27 KiB
C++
1106 lines
27 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
ImportMP3.cpp
|
|
|
|
Joshua Haberman
|
|
Leland Lucius
|
|
|
|
*//****************************************************************//**
|
|
|
|
\class MP3ImportFileHandle
|
|
\brief An ImportFileHandle for MP3 data
|
|
|
|
Audacity has finally moved to using a single mp3 library on all
|
|
platforms! It is the high performance, beautifully written libmad
|
|
(mpeg audio decoder). Finally there is harmony in the mp3 universe.
|
|
|
|
Much of this source code is based on 'minimad.c' as distributed
|
|
with libmad.
|
|
|
|
*//****************************************************************//**
|
|
|
|
\class MP3ImportPlugin
|
|
\brief An ImportPlugin for MP3 data
|
|
|
|
*//*******************************************************************/
|
|
|
|
#include "../Audacity.h" // for USE_* macros
|
|
|
|
#include <wx/defs.h>
|
|
|
|
#include "Import.h"
|
|
#include "ImportPlugin.h"
|
|
#include "../Project.h"
|
|
|
|
#define DESC XO("MP3 files")
|
|
|
|
static const auto exts =
|
|
{
|
|
wxT("mp3"),
|
|
wxT("mp2"),
|
|
wxT("mpa")
|
|
};
|
|
|
|
#ifndef USE_LIBMAD
|
|
|
|
static Importer::RegisteredUnusableImportPlugin registered
|
|
{
|
|
std::make_unique<UnusableImportPlugin>(DESC, FileExtensions(exts.begin(), exts.end()))
|
|
};
|
|
|
|
#else
|
|
|
|
#if defined(__WXMSW__)
|
|
#include <malloc.h>
|
|
#else
|
|
#include <stdlib.h>
|
|
#endif
|
|
|
|
#include <wx/file.h>
|
|
#include <wx/string.h>
|
|
|
|
#include "../Prefs.h"
|
|
#include "../Tags.h"
|
|
#include "../WaveTrack.h"
|
|
#include "../widgets/AudacityMessageBox.h"
|
|
#include "../widgets/ProgressDialog.h"
|
|
|
|
// PRL: include these last,
|
|
// and correct some preprocessor namespace pollution from wxWidgets that
|
|
// caused a warning about duplicate definition
|
|
#undef SIZEOF_LONG
|
|
extern "C"
|
|
{
|
|
#include "mad.h"
|
|
|
|
#ifdef USE_LIBID3TAG
|
|
#include <id3tag.h>
|
|
#endif
|
|
}
|
|
|
|
// Specifies the number of bytes in the input buffer. This also controls
|
|
// how many bytes will be scanned when searching for the first MP3 frame.
|
|
#define INPUT_BUFFER_SIZE 65535
|
|
|
|
// This is the number of decoded samples libmad adds at the beginning
|
|
// (This is an "observed" value.)
|
|
#define MAD_DELAY 529
|
|
|
|
class MP3ImportPlugin final : public ImportPlugin
|
|
{
|
|
public:
|
|
MP3ImportPlugin();
|
|
~MP3ImportPlugin();
|
|
|
|
wxString GetPluginStringID() override;
|
|
TranslatableString GetPluginFormatDescription() override;
|
|
std::unique_ptr<ImportFileHandle> Open(const FilePath &Filename, AudacityProject*) override;
|
|
};
|
|
|
|
using NewChannelGroup = std::vector< std::shared_ptr<WaveTrack> >;
|
|
|
|
class MP3ImportFileHandle final : public ImportFileHandle
|
|
{
|
|
public:
|
|
MP3ImportFileHandle(const FilePath &filename);
|
|
~MP3ImportFileHandle();
|
|
|
|
TranslatableString GetFileDescription() override;
|
|
ByteCount GetFileUncompressedBytes() override;
|
|
ProgressResult Import(WaveTrackFactory *trackFactory, TrackHolders &outTracks, Tags *tags) override;
|
|
wxInt32 GetStreamCount() override;
|
|
const TranslatableStrings &GetStreamInfo() override;
|
|
void SetStreamUsage(wxInt32 StreamID, bool Use) override;
|
|
|
|
private:
|
|
bool Open();
|
|
void CheckTags();
|
|
void CheckAPETags(bool atEnd);
|
|
void CheckID3V1Tags();
|
|
void CheckID3V2Tags(bool atEnd);
|
|
void CheckLyrics();
|
|
bool CheckMP3();
|
|
bool FillBuffer();
|
|
void LoadID3(Tags *tags);
|
|
|
|
// The MAD callbacks
|
|
|
|
static mad_flow input_cb(void *that,
|
|
struct mad_stream *stream);
|
|
mad_flow InputCB(struct mad_stream *stream);
|
|
|
|
static mad_flow filter_cb(void *that,
|
|
struct mad_stream const *stream,
|
|
struct mad_frame *frame);
|
|
mad_flow FilterCB(struct mad_stream const *stream, struct mad_frame *frame);
|
|
|
|
static mad_flow output_cb(void *that,
|
|
struct mad_header const *header,
|
|
struct mad_pcm *pcm);
|
|
mad_flow OutputCB(struct mad_header const *header, struct mad_pcm *pcm);
|
|
|
|
static mad_flow error_cb(void *that,
|
|
struct mad_stream *stream,
|
|
struct mad_frame *frame);
|
|
mad_flow ErrorCB(struct mad_stream *stream, struct mad_frame *frame);
|
|
|
|
private:
|
|
mad_decoder mDecoder;
|
|
|
|
wxFile mFile;
|
|
wxFileOffset mFilePos;
|
|
wxFileOffset mFileLen;
|
|
|
|
unsigned char mInputBuffer[INPUT_BUFFER_SIZE + MAD_BUFFER_GUARD];
|
|
int mInputBufferLen;
|
|
|
|
WaveTrackFactory *mTrackFactory;
|
|
NewChannelGroup mChannels;
|
|
unsigned mNumChannels;
|
|
|
|
ProgressResult mUpdateResult;
|
|
|
|
int mDelay;
|
|
int mPadding;
|
|
|
|
bool mHaveID3;
|
|
|
|
friend MP3ImportPlugin;
|
|
};
|
|
|
|
// ============================================================================
|
|
// MP3ImportPlugin
|
|
// ============================================================================
|
|
|
|
MP3ImportPlugin::MP3ImportPlugin()
|
|
: ImportPlugin(FileExtensions(exts.begin(), exts.end()))
|
|
{
|
|
}
|
|
|
|
MP3ImportPlugin::~MP3ImportPlugin()
|
|
{
|
|
}
|
|
|
|
wxString MP3ImportPlugin::GetPluginStringID()
|
|
{
|
|
return wxT("libmad");
|
|
}
|
|
|
|
TranslatableString MP3ImportPlugin::GetPluginFormatDescription()
|
|
{
|
|
return DESC;
|
|
}
|
|
|
|
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)
|
|
: ImportFileHandle(filename)
|
|
{
|
|
}
|
|
|
|
MP3ImportFileHandle::~MP3ImportFileHandle()
|
|
{
|
|
}
|
|
|
|
TranslatableString MP3ImportFileHandle::GetFileDescription()
|
|
{
|
|
return DESC;
|
|
}
|
|
|
|
auto MP3ImportFileHandle::GetFileUncompressedBytes() -> ByteCount
|
|
{
|
|
// TODO
|
|
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))
|
|
{
|
|
}
|
|
|
|
ProgressResult MP3ImportFileHandle::Import(WaveTrackFactory *trackFactory,
|
|
TrackHolders &outTracks,
|
|
Tags *tags)
|
|
{
|
|
outTracks.clear();
|
|
|
|
CreateProgress();
|
|
|
|
mTrackFactory = trackFactory;
|
|
mUpdateResult = ProgressResult::Success;
|
|
mNumChannels = 0;
|
|
|
|
// Set delay and padding to best possible in case the LAME tag is not present
|
|
mDelay = MAD_DELAY;
|
|
mPadding = 0;
|
|
|
|
// Initialize decoder
|
|
mad_decoder_init(&mDecoder, this, input_cb, 0, filter_cb, output_cb, error_cb, 0);
|
|
|
|
// Send the decoder on its way!
|
|
auto res = mad_decoder_run(&mDecoder, MAD_DECODER_MODE_SYNC);
|
|
|
|
// Terminate decoder
|
|
mad_decoder_finish(&mDecoder);
|
|
|
|
// Decoding failed, so pass it on
|
|
if (res != 0)
|
|
{
|
|
return ProgressResult::Failed;
|
|
}
|
|
|
|
// The user canceled the decoding, so bail without saving tracks or tags
|
|
if (mUpdateResult == ProgressResult::Cancelled)
|
|
{
|
|
return mUpdateResult;
|
|
}
|
|
|
|
// Flush and trim the channels
|
|
for (const auto &channel : mChannels)
|
|
{
|
|
channel->Flush();
|
|
|
|
// Trim any padding
|
|
if (mPadding)
|
|
{
|
|
double et = channel->GetEndTime();
|
|
double t1 = et - channel->LongSamplesToTime(mPadding);
|
|
channel->Clear(t1, et);
|
|
}
|
|
|
|
// And delay
|
|
if (mDelay)
|
|
{
|
|
double st = channel->GetStartTime();
|
|
double t0 = st + channel->LongSamplesToTime(mDelay);
|
|
channel->Clear(st, t0);
|
|
}
|
|
}
|
|
|
|
// Copy the WaveTrack pointers into the Track pointer list that
|
|
// we are expected to fill
|
|
outTracks.push_back(std::move(mChannels));
|
|
|
|
// Load ID3 tags from the file
|
|
LoadID3(tags);
|
|
|
|
return mUpdateResult;
|
|
}
|
|
|
|
bool MP3ImportFileHandle::Open()
|
|
{
|
|
mInputBufferLen = 0;
|
|
mFilePos = 0;
|
|
mHaveID3 = false;
|
|
|
|
// Open the file
|
|
if (!mFile.Open(mFilename))
|
|
{
|
|
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 for ID3 tags
|
|
CheckTags();
|
|
|
|
// Scan for the first MP3 frame
|
|
if (!CheckMP3())
|
|
{
|
|
mFile.Close();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void MP3ImportFileHandle::CheckTags()
|
|
{
|
|
// We do this twice to allow them to be in any order
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
CheckAPETags(false);
|
|
CheckID3V2Tags(false);
|
|
}
|
|
|
|
// We do this twice to allow them to be in any order. Even though ID3v1 is
|
|
// supposed to at the end, some apps put the v2 tags after the v1 tags.
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
CheckAPETags(true);
|
|
CheckID3V1Tags();
|
|
CheckLyrics();
|
|
CheckID3V2Tags(true);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
void MP3ImportFileHandle::CheckAPETags(bool atEnd)
|
|
{
|
|
int offset = atEnd ? mFileLen - 32 : mFilePos;
|
|
|
|
// Ensure file is positioned to start of (possible) tags
|
|
if (mFile.Seek(offset, wxFromStart) == wxInvalidOffset || mFile.Error())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// An APE tag header is 32 bytes
|
|
if (mFile.Read(mInputBuffer, 32) != 32 || mFile.Error())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Do we have an APE preamble?
|
|
if (memcmp(mInputBuffer, "APETAGEX", 8) != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get the (little endian) length
|
|
wxFileOffset len = (mInputBuffer[12] & 0xff) |
|
|
((mInputBuffer[13] & 0xff) << 8) |
|
|
((mInputBuffer[14] & 0xff) << 16) |
|
|
((mInputBuffer[15] & 0xff) << 24);
|
|
|
|
// Get needed flags
|
|
bool hasHeader = mInputBuffer[23] & 0x80;
|
|
|
|
// Skip the tags
|
|
if (!atEnd)
|
|
{
|
|
mFilePos += (32 + len);
|
|
}
|
|
else
|
|
{
|
|
mFileLen -= ((hasHeader ? 32 : 0) + len);
|
|
}
|
|
}
|
|
|
|
void MP3ImportFileHandle::CheckID3V1Tags()
|
|
{
|
|
// Ensure file is positioned to start of (possible) tags
|
|
if (mFile.Seek(mFileLen - 128, wxFromStart) == wxInvalidOffset || mFile.Error())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// An ID3v1 tag header is 3 bytes
|
|
if (mFile.Read(mInputBuffer, 3) != 3 || mFile.Error())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Do we have ID3v1 tags?
|
|
if (memcmp(mInputBuffer, "TAG", 3) != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Adjust file length
|
|
mFileLen -= 128;
|
|
|
|
// Remember that we have tags
|
|
mHaveID3 = true;
|
|
}
|
|
|
|
void MP3ImportFileHandle::CheckLyrics()
|
|
{
|
|
int offset = mFileLen - 9;
|
|
|
|
// Ensure file is positioned to start of (possible) lyrics
|
|
if (mFile.Seek(offset, wxFromStart) == wxInvalidOffset || mFile.Error())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// An Lyrics3 footeris 9 bytes
|
|
if (mFile.Read(mInputBuffer, 9) != 9 || mFile.Error())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Found a v1 Lyrics footer?
|
|
if (memcmp(mInputBuffer, "LYRICSEND", 9) == 0)
|
|
{
|
|
wxFileOffset pos = wxMax(offset - 5100, 0);
|
|
size_t len = offset - pos;
|
|
|
|
// Ensure file is positioned to start of (possible) lyrics
|
|
if (mFile.Seek(pos, wxFromStart) == wxInvalidOffset || mFile.Error())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Read the lyrics
|
|
if (mFile.Read(mInputBuffer, len) != len || mFile.Error())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Search forward to find the beginning of the lyrics
|
|
for (size_t i = 0; i < len; ++i)
|
|
{
|
|
if (memcmp(&mInputBuffer[i], "LYRICSBEGIN", 11) == 0)
|
|
{
|
|
// Adjust the file length to exclude the lyrics
|
|
mFileLen = pos + i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Found a v2 Lyrics footer?
|
|
else if (memcmp(mInputBuffer, "LYRICS200", 9) == 0)
|
|
{
|
|
// Ensure file is positioned to start of (possible) lyrics
|
|
if (mFile.Seek(-15, wxFromCurrent) == wxInvalidOffset || mFile.Error())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// An Lyrics3v2 length is 6 bytes
|
|
if (mFile.Read(mInputBuffer, 6) != 6 || mFile.Error())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Adjust the file length to exclude the lyrics
|
|
mInputBuffer[6] = 0;
|
|
mFileLen -= (wxAtoi((char *) mInputBuffer) + 15);
|
|
}
|
|
}
|
|
|
|
void MP3ImportFileHandle::CheckID3V2Tags(bool atEnd)
|
|
{
|
|
int offset = atEnd ? mFileLen - 10 : mFilePos;
|
|
|
|
// Ensure file is positioned to start of (possible) tags
|
|
if (mFile.Seek(offset, wxFromStart) == wxInvalidOffset || mFile.Error())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// An ID3v2 tag header is 10 bytes
|
|
if (mFile.Read(mInputBuffer, 10) != 10 || mFile.Error())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Do we have an ID3v2 header or footer?
|
|
if (memcmp(mInputBuffer, atEnd ? "3DI" : "ID3", 3) != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get and decode the length
|
|
wxFileOffset len = (mInputBuffer[6] & 0x7f);
|
|
len = (len << 7) | (mInputBuffer[7] & 0x7f);
|
|
len = (len << 7) | (mInputBuffer[8] & 0x7f);
|
|
len = (len << 7) | (mInputBuffer[9] & 0x7f);
|
|
|
|
// Skip the tags
|
|
if (!atEnd)
|
|
{
|
|
mFilePos += (10 + len);
|
|
}
|
|
else
|
|
{
|
|
mFileLen -= (10 + len + 10);
|
|
}
|
|
|
|
// Remember that we have tags
|
|
mHaveID3 = true;
|
|
}
|
|
|
|
bool MP3ImportFileHandle::CheckMP3()
|
|
{
|
|
wxFileOffset savedPos = mFilePos;
|
|
|
|
// Ensure file is positioned to start of 1st mp3 frame
|
|
if (mFile.Seek(mFilePos, wxFromStart) == wxInvalidOffset || mFile.Error())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Load as much as will fit into the buffer
|
|
if (!FillBuffer())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Initialize mad stream
|
|
mad_stream stream;
|
|
mad_stream_init(&stream);
|
|
mad_stream_buffer(&stream, mInputBuffer, mInputBufferLen);
|
|
|
|
// And header
|
|
mad_header header;
|
|
mad_header_init(&header);
|
|
|
|
// Scan the input buffer for 2 consecutive MP3 frames. When the header
|
|
// decoder finds a frame, it decodes it and ensures it is followed by
|
|
// another frame or EOF...thus 2 (or 1) consecutive frame(s) are detected.
|
|
int consecutive = 1;
|
|
while (consecutive > 0)
|
|
{
|
|
// Decode the header at the current stream position.
|
|
if (mad_header_decode(&header, &stream))
|
|
{
|
|
// End of buffer.
|
|
if (stream.error != MAD_ERROR_NONE)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
consecutive -= 1;
|
|
}
|
|
|
|
// Remember how many bytes were processed
|
|
int used = stream.this_frame - stream.buffer;
|
|
|
|
// Cleanup
|
|
mad_header_finish(&header);
|
|
mad_stream_finish(&stream);
|
|
|
|
// Did we find all that we wanted?
|
|
if (consecutive)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Reset file controls
|
|
mInputBufferLen = 0;
|
|
|
|
// Reposition file to start of mp3 frames to prepare for the Import.
|
|
mFilePos = savedPos + used;
|
|
if (mFile.Seek(mFilePos, wxFromStart) == wxInvalidOffset || mFile.Error())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Looks like an MP3...
|
|
return true;
|
|
}
|
|
|
|
bool MP3ImportFileHandle::FillBuffer()
|
|
{
|
|
// We either want enough to fill the input buffer or what's left in the file
|
|
auto want = wxMin(INPUT_BUFFER_SIZE - mInputBufferLen, mFileLen - mFilePos);
|
|
if (want > 0)
|
|
{
|
|
// We should always get what we ask for
|
|
auto got = mFile.Read(&mInputBuffer[mInputBufferLen], want);
|
|
if (got != want || mFile.Error())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Adjust input control
|
|
mInputBufferLen += got;
|
|
mFilePos += got;
|
|
}
|
|
|
|
// MAD requires that we add MAD_BUFFER_GUARD extra bytes when we've processed
|
|
// all of the MP3 frames. Otherwise, we will drop the last frame.
|
|
if (mFilePos == mFileLen)
|
|
{
|
|
memset(&mInputBuffer[mInputBufferLen], 0, MAD_BUFFER_GUARD);
|
|
mInputBufferLen += MAD_BUFFER_GUARD;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void MP3ImportFileHandle::LoadID3(Tags *tags)
|
|
{
|
|
#ifdef USE_LIBID3TAG
|
|
tags->Clear();
|
|
|
|
struct id3_file *id3file = NULL;
|
|
auto cleanup = finally([&]
|
|
{
|
|
if (id3file)
|
|
{
|
|
id3_file_close(id3file);
|
|
}
|
|
});
|
|
|
|
// Use id3_file_fdopen() instead of id3_file_open since wxWidgets can open a
|
|
// file with a Unicode name and id3_file_open() can't (under Windows).
|
|
id3file = id3_file_fdopen(mFile.fd(), ID3_FILE_MODE_READONLY);
|
|
if (!id3file)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The file descriptor is now owned by "id3file", so we must tell "mFile" to forget
|
|
// about it.
|
|
mFile.Detach();
|
|
|
|
// Load the tags
|
|
struct id3_tag *id3tags = id3_file_tag(id3file);
|
|
if (!id3tags)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Convert from libid3tag's ucs4 type to wxString.
|
|
//
|
|
// The ucs4 type is unsigned long which can be 8 bytes instead
|
|
// of the expected 4 bytes for a UTF-32 character, so we have
|
|
// to convert to unsigned int and then to wxString.
|
|
wxMBConvUTF32 converter;
|
|
auto toString = [=](const id3_ucs4_t *in)
|
|
{
|
|
// Count the number of characters
|
|
size_t len = 0;
|
|
for (const id3_ucs4_t *p = in; *p; p++)
|
|
{
|
|
len++;
|
|
}
|
|
|
|
// Would like to use std::dynarray or runtime-sized array,
|
|
// but VS doesn't support either.
|
|
wxUint32 *buf = (wxUint32 *) alloca((len + 1) * sizeof(wxUint32));
|
|
|
|
// Copy and convert to unsigned int
|
|
wxUint32 *out;
|
|
for (out = buf; *in; in++, out++)
|
|
{
|
|
*out = (wxUint32) (*in);
|
|
}
|
|
*out = 0;
|
|
|
|
// Finally convert to and return wxString
|
|
return wxString((char *) buf, converter);
|
|
};
|
|
|
|
// Extract tags from ID3 frames and add to our tags
|
|
bool have_year = false;
|
|
for (unsigned int i = 0; i < id3tags->nframes; ++i)
|
|
{
|
|
struct id3_frame *frame = id3tags->frames[i];
|
|
|
|
#if 0
|
|
wxLogDebug("ID: %08x '%4s'", (int) *(int *)frame->id, frame->id);
|
|
wxLogDebug("Desc: %s", frame->description);
|
|
wxLogDebug("Num fields: %d", frame->nfields);
|
|
|
|
for (unsigned int j = 0; j < frame->nfields; ++j)
|
|
{
|
|
wxLogDebug("field %d type %d", j, frame->fields[j].type);
|
|
if (frame->fields[j].type == ID3_FIELD_TYPE_STRINGLIST)
|
|
{
|
|
wxLogDebug("num strings %d", frame->fields[j].stringlist.nstrings);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
wxString n;
|
|
wxString v;
|
|
|
|
// Determine the tag name
|
|
if (strcmp(frame->id, ID3_FRAME_TITLE) == 0)
|
|
{
|
|
n = TAG_TITLE;
|
|
}
|
|
else if (strcmp(frame->id, ID3_FRAME_ARTIST) == 0)
|
|
{
|
|
n = TAG_ARTIST;
|
|
}
|
|
else if (strcmp(frame->id, ID3_FRAME_ALBUM) == 0)
|
|
{
|
|
n = TAG_ALBUM;
|
|
}
|
|
else if (strcmp(frame->id, ID3_FRAME_TRACK) == 0)
|
|
{
|
|
n = TAG_TRACK;
|
|
}
|
|
else if (strcmp(frame->id, ID3_FRAME_YEAR) == 0)
|
|
{
|
|
// LLL: When libid3tag encounters the "TYER" tag, it converts it to a
|
|
// "ZOBS" (obsolete) tag and adds a "TDRC" tag at the end of the
|
|
// list of tags using the first 4 characters of the "TYER" tag.
|
|
// Since we write both the "TDRC" and "TYER" tags, the "TDRC" tag
|
|
// will always be encountered first in the list. We want to use
|
|
// it since the converted "TYER" tag may have been truncated.
|
|
if (have_year)
|
|
{
|
|
continue;
|
|
}
|
|
n = TAG_YEAR;
|
|
have_year = true;
|
|
}
|
|
else if (strcmp(frame->id, ID3_FRAME_COMMENT) == 0)
|
|
{
|
|
n = TAG_COMMENTS;
|
|
}
|
|
else if (strcmp(frame->id, ID3_FRAME_GENRE) == 0)
|
|
{
|
|
n = TAG_GENRE;
|
|
}
|
|
else
|
|
{
|
|
// Use frame description as default tag name. The descriptions
|
|
// may include several "meanings" separated by "/" characters, so
|
|
// we just use the first meaning
|
|
n = UTF8CTOWX(frame->description).BeforeFirst(wxT('/'));
|
|
}
|
|
|
|
// Now get the tag value
|
|
const id3_ucs4_t *ustr = NULL;
|
|
|
|
if (n == TAG_COMMENTS)
|
|
{
|
|
ustr = id3_field_getfullstring(&frame->fields[3]);
|
|
}
|
|
else if (frame->nfields == 3)
|
|
{
|
|
ustr = id3_field_getstring(&frame->fields[1]);
|
|
if (ustr)
|
|
{
|
|
n = toString(ustr);
|
|
}
|
|
|
|
ustr = id3_field_getstring(&frame->fields[2]);
|
|
}
|
|
else if (frame->nfields >= 2)
|
|
{
|
|
ustr = id3_field_getstrings(&frame->fields[1], 0);
|
|
}
|
|
|
|
// Convert the value
|
|
if (ustr)
|
|
{
|
|
v = toString(ustr);
|
|
}
|
|
|
|
// And add it to the list of tags
|
|
if (!n.empty() && !v.empty())
|
|
{
|
|
tags->SetTag(n, v);
|
|
}
|
|
}
|
|
|
|
// Convert v1 genre to name
|
|
if (tags->HasTag(TAG_GENRE))
|
|
{
|
|
long g = -1;
|
|
if (tags->GetTag(TAG_GENRE).ToLong(&g))
|
|
{
|
|
tags->SetTag(TAG_GENRE, tags->GetGenre(g));
|
|
}
|
|
}
|
|
#else
|
|
(void) tags;
|
|
#endif
|
|
}
|
|
|
|
//
|
|
// MAD Callbacks
|
|
//
|
|
|
|
// The input callback is called when the decoder wants more data
|
|
mad_flow MP3ImportFileHandle::input_cb(void *that,
|
|
struct mad_stream *stream)
|
|
{
|
|
auto cb = [&]()
|
|
{
|
|
return ((MP3ImportFileHandle *) that)->InputCB(stream);
|
|
};
|
|
|
|
return GuardedCall<mad_flow>(cb, MakeSimpleGuard(MAD_FLOW_BREAK));
|
|
}
|
|
|
|
mad_flow MP3ImportFileHandle::InputCB(struct mad_stream *stream)
|
|
{
|
|
// Update the progress
|
|
mUpdateResult = mProgress->Update((wxLongLong_t) mFilePos, (wxLongLong_t) mFileLen);
|
|
if (mUpdateResult != ProgressResult::Success)
|
|
{
|
|
return MAD_FLOW_STOP;
|
|
}
|
|
|
|
// Stop if we've consumed all of the MP3 data
|
|
if (mFilePos == mFileLen)
|
|
{
|
|
return MAD_FLOW_STOP;
|
|
}
|
|
|
|
// "Each time you refill your buffer, you need to preserve the data in
|
|
// your existing buffer from stream.next_frame to the end.
|
|
//
|
|
// This usually amounts to calling memmove() on this unconsumed portion
|
|
// of the buffer and appending NEW data after it, before calling
|
|
// mad_stream_buffer()
|
|
// -- Rob Leslie, on the mad-dev mailing list
|
|
if (stream->next_frame)
|
|
{
|
|
mInputBufferLen -= (stream->next_frame - mInputBuffer);
|
|
memmove(mInputBuffer, stream->next_frame, mInputBufferLen);
|
|
}
|
|
|
|
// Refill the buffer
|
|
if (!FillBuffer())
|
|
{
|
|
return MAD_FLOW_BREAK;
|
|
}
|
|
|
|
// And give it back to MAD
|
|
mad_stream_buffer(stream, mInputBuffer, mInputBufferLen);
|
|
|
|
return MAD_FLOW_CONTINUE;
|
|
}
|
|
|
|
// The filter callback lets us examine each frame and decide if it should be
|
|
// kept or tossed. We use this to detect the Xing or LAME tags.
|
|
mad_flow MP3ImportFileHandle::filter_cb(void *that,
|
|
struct mad_stream const *stream,
|
|
struct mad_frame *frame)
|
|
{
|
|
auto cb = [&]()
|
|
{
|
|
return ((MP3ImportFileHandle *) that)->FilterCB(stream, frame);
|
|
};
|
|
|
|
return GuardedCall<mad_flow>(cb, MakeSimpleGuard(MAD_FLOW_BREAK));
|
|
}
|
|
|
|
mad_flow MP3ImportFileHandle::FilterCB(struct mad_stream const *stream,
|
|
struct mad_frame *frame)
|
|
{
|
|
// We only want to jinspect the first frame, so disable future calls
|
|
mDecoder.filter_func = nullptr;
|
|
|
|
// Is it a VBRI info frame?
|
|
if (memcmp(&stream->this_frame[4 + 32], "VBRI", 4) == 0)
|
|
{
|
|
mDelay = (stream->this_frame[4 + 32 + 6] & 0xff) << 8 |
|
|
(stream->this_frame[4 + 32 + 7] & 0xff);
|
|
|
|
return MAD_FLOW_CONTINUE;
|
|
}
|
|
|
|
// Look for Xing/Info information
|
|
|
|
// Get the ancillary data ptr and length. If the frame has CRC protection, we make
|
|
// a small adjustment to get around an apparent bug in libmad.
|
|
auto ptr = stream->anc_ptr.byte - (frame->header.flags & MAD_FLAG_PROTECTION ? 2 : 0);
|
|
int len = stream->anc_bitlen / 8;
|
|
|
|
// Ensure it's something we can understand
|
|
if (len < 4 || (memcmp(ptr, "Xing", 4) != 0 && memcmp(ptr, "Info", 4) != 0))
|
|
{
|
|
return MAD_FLOW_CONTINUE;
|
|
}
|
|
|
|
// Skip the tag
|
|
ptr += 4;
|
|
len -= 4;
|
|
|
|
enum flagBits
|
|
{
|
|
hasFrames = 0x0001,
|
|
hasBytes = 0x0002,
|
|
hasToc = 0x0004,
|
|
hasScale = 0x0008
|
|
};
|
|
|
|
// Extract the flags
|
|
unsigned int flags = (((((ptr[0] << 8) + ptr[1]) << 8) + ptr[2]) << 8) + ptr[3];
|
|
ptr += 4;
|
|
len -= 4;
|
|
|
|
// Skip the number of frames
|
|
if (len >= 4 && flags & hasFrames)
|
|
{
|
|
ptr += 4;
|
|
len -= 4;
|
|
}
|
|
|
|
// Skip the number of bytes
|
|
if (len >= 4 && flags & hasBytes)
|
|
{
|
|
ptr += 4;
|
|
len -= 4;
|
|
}
|
|
|
|
// Skip the TOC
|
|
if (len >= 100 && flags & hasToc)
|
|
{
|
|
ptr += 100;
|
|
len -= 100;
|
|
}
|
|
|
|
// Skip the VBR Scale
|
|
if (len >= 4 && flags && hasScale)
|
|
{
|
|
ptr += 4;
|
|
len -= 4;
|
|
}
|
|
|
|
// Bail if LAME wasn't the encoder or we don't have enough ancillary data left
|
|
if (len < 24 || memcmp(ptr, "LAME", 4) != 0)
|
|
{
|
|
return MAD_FLOW_IGNORE;
|
|
}
|
|
|
|
// Skip down to the delay and padding
|
|
ptr += 21;
|
|
len -= 21;
|
|
|
|
// Extract the delay and padding and adjust for decoder delay
|
|
mDelay = (ptr[0] << 4) + (ptr[1] >> 4) + MAD_DELAY;
|
|
mPadding = ((ptr[1] & 0x0f) << 8) + ptr[2] - MAD_DELAY;
|
|
if (mPadding < 0)
|
|
{
|
|
mPadding = 0;
|
|
}
|
|
|
|
return MAD_FLOW_IGNORE;
|
|
}
|
|
|
|
// The output callback is called every time the decoder has finished decoding
|
|
// a frame, allowing us to use the decoded data
|
|
mad_flow MP3ImportFileHandle::output_cb(void *that,
|
|
struct mad_header const *header,
|
|
struct mad_pcm *pcm)
|
|
{
|
|
auto cb = [&]()
|
|
{
|
|
return ((MP3ImportFileHandle *) that)->OutputCB(header, pcm);
|
|
};
|
|
|
|
return GuardedCall<mad_flow>(cb, MakeSimpleGuard(MAD_FLOW_BREAK));
|
|
}
|
|
|
|
enum mad_flow MP3ImportFileHandle::OutputCB(struct mad_header const * WXUNUSED(header),
|
|
struct mad_pcm *pcm)
|
|
{
|
|
// If this is the first run, we need to create the WaveTracks that
|
|
// will hold the data. We do this now because now is the first
|
|
// moment when we know how many channels there are.
|
|
if (mChannels.empty())
|
|
{
|
|
mNumChannels = pcm->channels;
|
|
|
|
mChannels.resize(mNumChannels);
|
|
|
|
for (auto &channel: mChannels)
|
|
{
|
|
// Mad library header explains the 32 bit fixed point format with
|
|
// 28 fractional bits. Effective sample format must therefore be
|
|
// more than 24, and this is our only choice now.
|
|
channel = NewWaveTrack(*mTrackFactory, floatSample, pcm->samplerate);
|
|
}
|
|
}
|
|
|
|
// Get the number of samples in each channel
|
|
auto samples = pcm->length;
|
|
|
|
// Convert libmad samples to float and append to WaveTracks
|
|
for (int chn = 0; chn < mNumChannels; ++chn)
|
|
{
|
|
// Number of samples will never be more than 1152
|
|
float sampleBuf[1152];
|
|
wxASSERT(samples <= 1152);
|
|
|
|
// Copy over the samples
|
|
for (int sample = 0; sample < samples; ++sample)
|
|
{
|
|
// Convert libmad's fixed point representation to float
|
|
sampleBuf[sample] = ((float) pcm->samples[chn][sample] / (1L << MAD_F_FRACBITS));
|
|
}
|
|
|
|
// And append to the channel
|
|
mChannels[chn]->Append((samplePtr) sampleBuf, floatSample, samples);
|
|
}
|
|
|
|
return MAD_FLOW_CONTINUE;
|
|
}
|
|
|
|
// The error callback is used when MAD encounters an error and needs to know
|
|
// how it should proceed
|
|
mad_flow MP3ImportFileHandle::error_cb(void *that,
|
|
struct mad_stream *stream,
|
|
struct mad_frame *frame)
|
|
{
|
|
auto cb = [&]()
|
|
{
|
|
return ((MP3ImportFileHandle *) that)->ErrorCB(stream, frame);
|
|
};
|
|
|
|
return GuardedCall<mad_flow>(cb, MakeSimpleGuard(MAD_FLOW_BREAK));
|
|
}
|
|
|
|
enum mad_flow MP3ImportFileHandle::ErrorCB(struct mad_stream *stream,
|
|
struct mad_frame *frame)
|
|
{
|
|
// You always get a LOSTSYNC error at EOF, so just ignore it
|
|
if (stream->error == MAD_ERROR_LOSTSYNC && mFilePos == mFileLen)
|
|
{
|
|
return MAD_FLOW_CONTINUE;
|
|
}
|
|
|
|
// This can happen when parsing the first frame. We can use the number of channels
|
|
// to test for this since it hasn't been determined yet.
|
|
if (stream->error == MAD_ERROR_BADDATAPTR && mNumChannels == 0)
|
|
{
|
|
return MAD_FLOW_CONTINUE;
|
|
}
|
|
|
|
// Let the user know about the error
|
|
AudacityMessageBox(XO("Import failed\n\nThis is likely caused by a malformed MP3.\n\n"));
|
|
|
|
return MAD_FLOW_BREAK;
|
|
}
|
|
|
|
#endif
|