diff options
Diffstat (limited to 'Src/Plugins/Input/in_mp3/Metadata.cpp')
-rw-r--r-- | Src/Plugins/Input/in_mp3/Metadata.cpp | 616 |
1 files changed, 616 insertions, 0 deletions
diff --git a/Src/Plugins/Input/in_mp3/Metadata.cpp b/Src/Plugins/Input/in_mp3/Metadata.cpp new file mode 100644 index 00000000..89e87697 --- /dev/null +++ b/Src/Plugins/Input/in_mp3/Metadata.cpp @@ -0,0 +1,616 @@ +#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);
\ No newline at end of file |