aboutsummaryrefslogtreecommitdiff
path: root/Src/Plugins/Input/in_mp3/Metadata.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Src/Plugins/Input/in_mp3/Metadata.cpp')
-rw-r--r--Src/Plugins/Input/in_mp3/Metadata.cpp616
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