winamp/Src/Plugins/Input/in_mp3/Metadata.cpp
2024-09-24 14:54:57 +02:00

616 lines
15 KiB
C++

#include "Metadata.h"
#include "main.h"
#include "api__in_mp3.h"
#include "LAMEInfo.h"
#include "AACFrame.h"
#include "config.h"
#include "LAMEInfo.h"
#include <shlwapi.h>
#include <assert.h>
#include <foundation/error.h>
#include <strsafe.h>
#define INFO_READ_SIZE 32768
Metadata::Metadata( CGioFile *_file, const wchar_t *_filename )
{
if ( !PathIsURL( _filename ) )
filename = _wcsdup( _filename );
ReadTags( _file );
if ( bitrate = _file->GetAvgVBRBitrate() * 1000 )
{
length_ms = _file->m_vbr_ms;
vbr = _file->m_vbr_flag || _file->m_vbr_hdr;
}
}
void GetFileDescription(const wchar_t *file, CGioFile &_file, wchar_t *data, size_t datalen);
void GetAudioInfo(const wchar_t *filename, CGioFile *file, int *len, int *channels, int *bitrate, int *vbr, int *sr);
int Metadata::Open(const wchar_t *_filename)
{
if ( filename && *filename )
free( filename );
filename = _wcsdup(_filename);
if (file.Open(filename, INFO_READ_SIZE/1024) != NErr_Success)
return 1;
GetAudioInfo(filename, &file, &length_ms, &channels, &bitrate, &vbr, &sampleRate);
ReadTags(&file);
file.Close();
return METADATA_SUCCESS;
}
Metadata::~Metadata()
{
if (filename)
{
free(filename);
filename=0;
}
}
void Metadata::ReadTags(CGioFile *_file)
{
// Process ID3v1
if (config_parse_id3v1)
{
void *id3v1_data = _file->GetID3v1();
if (id3v1_data)
id3v1.Decode(id3v1_data);
}
if (config_parse_id3v2)
{
uint32_t len = 0;
void *id3v2_data = _file->GetID3v2(&len);
if (id3v2_data)
id3v2.Decode(id3v2_data, len);
}
if (config_parse_lyrics3)
{
uint32_t len = 0;
void *lyrics3_data = _file->GetLyrics3(&len);
if (lyrics3_data)
lyrics3.Decode(lyrics3_data, len);
}
if (config_parse_apev2)
{
uint32_t len = 0;
void *apev2_data = _file->GetAPEv2(&len);
if (apev2_data)
apev2.Decode(apev2_data, len);
}
}
static int ID3Write(const wchar_t *filename, HANDLE infile, DWORD offset, void *data, DWORD len)
{
wchar_t tempFile[MAX_PATH] = {0};
StringCchCopyW(tempFile, MAX_PATH, filename);
PathRemoveExtension(tempFile);
StringCchCatW(tempFile, MAX_PATH, L".tmp");
// check to make sure the filename was actually different!
// benski> TODO: we should just try to mangle the filename more rather than totally bail out
if (!_wcsicmp(tempFile, filename))
return SAVE_ERROR_CANT_OPEN_TEMPFILE;
// TODO: overlapped I/O
HANDLE outfile = CreateFile(tempFile, GENERIC_WRITE|GENERIC_READ, FILE_SHARE_READ, 0, CREATE_ALWAYS, FILE_FLAG_SEQUENTIAL_SCAN, 0);
if (outfile != INVALID_HANDLE_VALUE)
{
DWORD written=0;
if (data && len)
WriteFile(outfile, data, len, &written, NULL);
SetFilePointer(infile, offset, 0, FILE_BEGIN);
DWORD read=0;
do
{
char data[4096] = {0};
written = read = 0;
ReadFile(infile, data, 4096, &read, NULL);
if (read) WriteFile(outfile, data, read, &written, NULL);
}
while (read != 0);
CloseHandle(outfile);
CloseHandle(infile);
if (!MoveFile(tempFile, filename))
{
if (!CopyFile(tempFile, filename, FALSE))
{
DeleteFile(tempFile);
return SAVE_ERROR_ERROR_OVERWRITING;
}
DeleteFile(tempFile);
}
return SAVE_SUCCESS;
}
return SAVE_ERROR_CANT_OPEN_TEMPFILE;
}
bool Metadata::IsDirty()
{
return id3v1.IsDirty() || id3v2.IsDirty() || lyrics3.IsDirty() || apev2.IsDirty();
}
int Metadata::Save()
{
if (!IsDirty())
return SAVE_SUCCESS;
int err=SAVE_SUCCESS;
if (GetFileAttributes(filename)&FILE_ATTRIBUTE_READONLY)
return SAVE_ERROR_READONLY;
HANDLE metadataFile = CreateFile(filename, GENERIC_WRITE|GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0);
if (metadataFile == INVALID_HANDLE_VALUE)
return SAVE_ERROR_OPENING_FILE;
if (file.Open(filename, INFO_READ_SIZE/1024) != NErr_Success)
{
CloseHandle(metadataFile);
return SAVE_ERROR_OPENING_FILE;
}
bool strippedID3v1=false; // this flag will get set to true when we remove ID3v1 as a side effect of removing APEv2 or Lyrics3 (or ID3v2.4 end-tag if/when we implement)
bool strippedLyrics3=false;
/* Strip APEv2 */
if (config_parse_apev2 && config_write_apev2 && apev2.IsDirty())
{
uint32_t len = 0;
void *apev2_data = file.GetAPEv2(&len);
if (apev2_data)
{
uint32_t lyrics3_len = 0;
void *lyrics3_data = file.GetLyrics3(&lyrics3_len);
if (lyrics3_data)
SetFilePointer(metadataFile, -(LONG)(len + 15 + lyrics3_len + (file.GetID3v1()?128:0)), NULL, FILE_END);
else
SetFilePointer(metadataFile, -(LONG)(len + (file.GetID3v1()?128:0)), NULL, FILE_END);
SetEndOfFile(metadataFile);
strippedLyrics3=true;
strippedID3v1=true;
}
}
/* Strip Lyrics3 tag */
if (!strippedLyrics3 && config_parse_lyrics3 && lyrics3.IsDirty())
{
uint32_t len = 0;
void *lyrics3_data = file.GetLyrics3(&len);
if (lyrics3_data)
{
SetFilePointer(metadataFile, -(LONG)(len + 15 + (file.GetID3v1()?128:0)), NULL, FILE_END);
SetEndOfFile(metadataFile);
strippedID3v1=true;
}
}
/* Strip ID3v1(.1) tag */
if (!strippedID3v1 /* if we stripped lyrics3 tag, then we ended up stripping id3v1 also */
&& config_parse_id3v1 && config_write_id3v1 && id3v1.IsDirty())
{
if (file.GetID3v1()) // see if we have ID3v1
{
SetFilePointer(metadataFile, -128, NULL, FILE_END);
SetEndOfFile(metadataFile);
}
}
/* Write APEv2 */
if (config_parse_apev2 && config_write_apev2 && apev2.IsDirty() && apev2.HasData())
{
switch(config_apev2_header)
{
case ADD_HEADER:
apev2.SetFlags(APEv2::FLAG_HEADER_HAS_HEADER, APEv2::FLAG_HEADER_HAS_HEADER);
break;
case REMOVE_HEADER:
apev2.SetFlags(0, APEv2::FLAG_HEADER_HAS_HEADER);
break;
}
size_t apev2_len = apev2.EncodeSize();
void *apev2_data = malloc(apev2_len);
if (apev2_data && apev2.Encode(apev2_data, apev2_len) == APEv2::APEV2_SUCCESS)
{
SetFilePointer(metadataFile, 0, NULL, FILE_END);
DWORD bytesWritten=0;
WriteFile(metadataFile, apev2_data, (DWORD)apev2_len, &bytesWritten, 0);
free(apev2_data);
apev2_data = 0;
if (bytesWritten != apev2_len)
{
err=SAVE_APEV2_WRITE_ERROR;
goto fail;
}
}
else
{
free(apev2_data);
apev2_data = 0;
err=SAVE_APEV2_WRITE_ERROR;
goto fail;
}
}
/* Write Lyrics3 */
if (strippedLyrics3) /* if we need to rewrite it because we stripped it (e.g. removing an APEv2 tag)*/
{
/* since we don't modify lyrics3 (yet) we'll just rewrite the original binary data */
uint32_t len = 0;
void *lyrics3_data = file.GetLyrics3(&len);
if (lyrics3_data)
{
SetFilePointer(metadataFile, 0, NULL, FILE_END);
DWORD bytesWritten=0;
WriteFile(metadataFile, lyrics3_data, len, &bytesWritten, NULL);
if (bytesWritten != len)
{
err=SAVE_LYRICS3_WRITE_ERROR;
goto fail;
}
char temp[7] = {0};
StringCchPrintfA(temp, 7, "%06u", len);
bytesWritten = 0;
WriteFile(metadataFile, temp, 6, &bytesWritten, NULL);
if (bytesWritten != 6)
{
err=SAVE_LYRICS3_WRITE_ERROR;
goto fail;
}
bytesWritten = 0;
WriteFile(metadataFile, "LYRICS200", 9, &bytesWritten, NULL);
if (bytesWritten != 9)
{
err=SAVE_LYRICS3_WRITE_ERROR;
goto fail;
}
}
}
/* Write ID3v1 */
if (config_parse_id3v1 && config_write_id3v1 && id3v1.IsDirty())
{
uint8_t id3v1_data[128] = {0};
if (id3v1.Encode(id3v1_data) == METADATA_SUCCESS)
{
SetFilePointer(metadataFile, 0, NULL, FILE_END);
DWORD bytesWritten=0;
WriteFile(metadataFile, id3v1_data, 128, &bytesWritten, NULL);
if (bytesWritten != 128)
{
err=SAVE_ID3V1_WRITE_ERROR;
goto fail;
}
}
}
else if (strippedID3v1)
{
/** if we stripped lyrics3 or apev2 but didn't modify id3v1 (or are configured not to use it),
** we need to rewrite it back to the original data
**/
void *id3v1_data=file.GetID3v1();
if (id3v1_data)
{
SetFilePointer(metadataFile, 0, NULL, FILE_END);
DWORD bytesWritten=0;
WriteFile(metadataFile, id3v1_data, 128, &bytesWritten, NULL);
if (bytesWritten != 128)
{
err=SAVE_ID3V1_WRITE_ERROR;
goto fail;
}
}
}
/* Write ID3v2 */
if (config_parse_id3v2 && config_write_id3v2 && id3v2.IsDirty())
{
uint32_t oldlen=0;
void *old_id3v2_data = file.GetID3v2(&oldlen);
id3v2.id3v2.SetPadding(false); // turn off padding to see if we can get away with non re-writing the file
uint32_t newlen = id3v2.EncodeSize();
if (old_id3v2_data && !newlen) // there's an old tag, but no new tag
{
err = ID3Write(filename, metadataFile, oldlen, 0, 0);
if (err == SAVE_SUCCESS)
metadataFile = INVALID_HANDLE_VALUE; // ID3Write returns true if it closed the handle
else
goto fail;
}
else if (!old_id3v2_data && !newlen) // no old tag, no new tag.. easy :)
{
}
else
{
id3v2.id3v2.SetPadding(true);
if (newlen <= oldlen) // if we can fit in the old tag
{
if (oldlen != newlen)
id3v2.id3v2.ForcePading(oldlen-newlen); // pad out the rest of the tag
else
id3v2.id3v2.SetPadding(false);
assert(id3v2.EncodeSize() == oldlen);
newlen = oldlen;
uint8_t *new_id3v2_data = (uint8_t *)calloc(newlen, sizeof(uint8_t));
if (new_id3v2_data && id3v2.Encode(new_id3v2_data, newlen) == METADATA_SUCCESS)
{
// TODO: deal with files with multiple starting id3v2 tags
SetFilePointer(metadataFile, 0, NULL, FILE_BEGIN);
DWORD bytesWritten=0;
WriteFile(metadataFile, new_id3v2_data, newlen, &bytesWritten, NULL);
free(new_id3v2_data);
new_id3v2_data = 0;
if (bytesWritten != newlen)
{
err = SAVE_ID3V2_WRITE_ERROR;
goto fail;
}
}
else
{
free(new_id3v2_data);
new_id3v2_data = 0;
err = SAVE_ID3V2_WRITE_ERROR;
goto fail;
}
}
else // otherwise we have to pad out the start
{
newlen = id3v2.EncodeSize();
uint8_t *new_id3v2_data = (uint8_t *)calloc(newlen, sizeof(uint8_t));
if (new_id3v2_data && id3v2.Encode(new_id3v2_data, newlen) == METADATA_SUCCESS)
{
// TODO: deal with files with multiple starting id3v2 tags
SetFilePointer(metadataFile, 0, NULL, FILE_BEGIN);
DWORD bytesWritten=0;
err = ID3Write(filename, metadataFile, oldlen, new_id3v2_data, newlen);
free(new_id3v2_data);
new_id3v2_data = 0;
if (err == SAVE_SUCCESS)
metadataFile = INVALID_HANDLE_VALUE; // ID3Write returns true if it closed the handle
else
goto fail;
}
else
{
free(new_id3v2_data);
new_id3v2_data = 0;
err = SAVE_ID3V2_WRITE_ERROR;
goto fail;
}
}
}
}
fail:
file.Close();
if (metadataFile != INVALID_HANDLE_VALUE)
CloseHandle(metadataFile);
return err;
}
int Metadata::GetExtendedData(const char *tag, wchar_t *data, int dataLen)
{
int understood=0;
switch (id3v2.GetString(tag, data, dataLen))
{
case -1:
data[0]=0;
understood=1;
break;
case 1:
return 1;
}
switch (apev2.GetString(tag, data, dataLen))
{
case -1:
data[0]=0;
understood=1;
break;
case 1:
return 1;
}
switch (lyrics3.GetString(tag, data, dataLen))
{
case -1:
data[0]=0;
understood=1;
break;
case 1:
return 1;
}
switch (id3v1.GetString(tag, data, dataLen))
{
case -1:
data[0]=0;
understood=1;
break;
case 1:
return 1;
}
switch (GetString(tag, data, dataLen))
{
case -1:
data[0]=0;
understood=1;
break;
case 1:
return 1;
}
return understood;
}
int Metadata::SetExtendedData(const char *tag, const wchar_t *data)
{
int understood=0;
if (config_create_id3v2 || id3v2.HasData())
understood |= id3v2.SetString(tag, data);
if (config_create_apev2 || apev2.HasData())
understood |= apev2.SetString(tag, data);
if (config_create_id3v1 || id3v1.HasData())
understood |= id3v1.SetString(tag, data);
return understood;
}
int Metadata::GetString(const char *tag, wchar_t *data, int dataLen)
{
if (!_stricmp(tag, "formatinformation"))
{
data[0]=0;
if (filename)
{
if (file.Open(filename, INFO_READ_SIZE/1024) == NErr_Success)
GetFileDescription(filename, file, data, dataLen);
file.Close();
}
}
else if (!_stricmp(tag, "length"))
{
StringCchPrintfW(data, dataLen, L"%d", length_ms);
}
else if (!_stricmp(tag, "stereo"))
{
StringCchPrintfW(data, dataLen, L"%d", channels==2);
}
else if (!_stricmp(tag, "vbr"))
{
StringCchPrintfW(data, dataLen, L"%d", vbr);
}
else if (!_stricmp(tag, "bitrate"))
{
StringCchPrintfW(data, dataLen, L"%d", bitrate/1000);
}
else if (!_stricmp(tag, "gain"))
{
StringCchPrintfW(data, dataLen, L"%-+.2f dB", file.GetGain());
}
else if (!_stricmp(tag, "pregap"))
{
if (file.prepad)
{
StringCchPrintfW(data, dataLen, L"%u", file.prepad);
return 1;
}
return -1;
}
else if (!_stricmp(tag, "postgap"))
{
if (file.prepad) // yes, we check for this because postpad could legitimately be 0
{
StringCchPrintfW(data, dataLen, L"%u", file.postpad);
return 1;
}
return -1;
}
else if (!_stricmp(tag, "numsamples"))
{
if (file.m_vbr_samples)
{
StringCchPrintfW(data, dataLen, L"%I64u", file.m_vbr_samples);
return 1;
}
return -1;
}
else if (!_stricmp(tag, "endoffset"))
{
if (file.m_vbr_frames)
{
int totalFrames = file.m_vbr_frames;
if (totalFrames > 8)
{
int seekPoint = 0;
// we're using m_vbr_bytes here instead of file.ContentLength(), because we're already trusting the other LAME header info
#define MAX_SIZE_8_FRAMES (1448 * 8) // mp3 frames won't be ever be any bigger than this (320kbps 32000Hz + padding)
if (file.m_vbr_bytes > MAX_SIZE_8_FRAMES)
seekPoint = (int)(file.m_vbr_bytes - MAX_SIZE_8_FRAMES);
else
seekPoint = 0;
size_t offsets[8] = {0};
size_t offsetsRead = 0;
size_t offsetPosition = 0;
unsigned char header[6] = {0};
MPEGFrame frame;
if (file.Open(filename, INFO_READ_SIZE/1024) != NErr_Success)
return -1;
// first we need to sync
while (1)
{
file.SetCurrentPosition(seekPoint, CGioFile::GIO_FILE_BEGIN);
int read = 0;
file.Read(header, 6, &read);
if (read != 6)
break;
frame.ReadBuffer(header);
if (frame.IsSync() && frame.GetLayer() == 3)
{
// make sure this isn't false sync - see if we can get another sync...
int nextPoint = seekPoint + frame.FrameSize();
file.SetCurrentPosition(nextPoint, CGioFile::GIO_FILE_BEGIN);
file.Read(header, 6, &read);
if (read != 6) // must be EOF
break;
frame.ReadBuffer(header);
if (frame.IsSync() && frame.GetLayer() == 3)
break;
}
seekPoint++;
}
while (1)
{
file.SetCurrentPosition(seekPoint, CGioFile::GIO_FILE_BEGIN);
int read = 0;
file.Read(header, 6, &read);
if (read != 6)
break;
frame.ReadBuffer(header);
if (frame.IsSync() && frame.GetLayer() == 3)
{
offsets[offsetPosition] = seekPoint;
offsetPosition = (offsetPosition + 1) % 8;
offsetsRead++;
seekPoint += frame.FrameSize();
}
else
break;
}
if (offsetsRead >= 8)
{
StringCchPrintfW(data, dataLen, L"%I32d", offsets[offsetPosition] + file.m_vbr_frame_len);
file.Close();
return 1;
}
file.Close();
}
}
return -1;
}
else
return 0;
return 1;
}
int fixAACCBRbitrate(int br);