Files
tlib/oversampling/WDL/metadata.h
2024-05-24 13:28:31 +02:00

2242 lines
59 KiB
C

#ifndef _WDL_METADATA_H_
#define _WDL_METADATA_H_
#include <math.h>
#include "wdlstring.h"
#include "xmlparse.h"
#include "fileread.h"
#include "filewrite.h"
#include "queue.h"
#include "win32_utf8.h"
#include "wdl_base64.h"
#include "coreaudio_channel_formats.h"
const char *EnumMexKeys(int i, const char **desc=NULL)
{
// TO_DO_IF_METADATA_UPDATE
static const char *s_mexkeys[]=
{"TITLE", "ARTIST", "ALBUM", "TRACKNUMBER", "YEAR", "GENRE", "COMMENT", "DESC", "BPM", "KEY", "DB_CUSTOM"};
static const char *s_mexdesc[]=
{"Title", "Artist", "Album", "Track", "Date", "Genre", "Comment", "Description", "BPM", "Key", "Media Explorer Tags"};
bool ok = i >= 0 && i < sizeof(s_mexkeys)/sizeof(s_mexkeys[0]);
if (desc) *desc = ok ? s_mexdesc[i] : NULL;
return ok ? s_mexkeys[i] : NULL;
}
int MetadataToArray(WDL_StringKeyedArray<char*> *metadata, WDL_TypedBuf<const char*> *metadata_arr)
{
if (!metadata || !metadata_arr) return 0;
int cnt=0;
for (int i=0; i < metadata->GetSize(); ++i)
{
const char *k, *v=metadata->Enumerate(i, &k);
if (k && v)
{
metadata_arr->Add(k);
metadata_arr->Add(v);
++cnt;
}
}
metadata_arr->Add(NULL);
return cnt;
}
int ArrayToMetadata(const char **metadata_arr, WDL_StringKeyedArray<char*> *metadata)
{
if (!metadata_arr || !metadata) return 0;
int cnt=0;
for (; metadata_arr[0] && metadata_arr[1]; metadata_arr += 2)
{
metadata->AddUnsorted(metadata_arr[0], strdup(metadata_arr[1]));
++cnt;
}
if (cnt) metadata->Resort();
return cnt;
}
char *tag_strndup(const char *src, int len)
{
if (!src || !len) return NULL;
int n=0;
while (n < len && src[n]) ++n;
char *dest=(char*)malloc(n+1);
if (!dest) return NULL;
memcpy(dest, src, n);
dest[n]=0;
return dest;
}
WDL_UINT64 ParseUInt64(const char *val)
{
WDL_UINT64 i=0;
if (val)
{
const char *p=val;
while (*p)
{
int d=*p-'0';
if (d < 0 || d > 9) break;
i=(10*i)+d;
++p;
}
if (*p) i=0;
}
return i;
}
void InsertMetadataIncrKeyIfNeeded(WDL_StringKeyedArray<char*> *metadata,
const char *key, const char *val)
{
if (!metadata->Exists(key))
{
metadata->Insert(key, strdup(val));
}
else
{
for (int i=2; i < 100; ++i)
{
char str[2048];
snprintf(str,sizeof(str), "%s:%d", key, i);
if (!metadata->Exists(str))
{
metadata->Insert(str, strdup(val));
break;
}
}
}
}
void XMLCompliantAppend(WDL_FastString *str, const char *txt, bool is_value)
{
if (str && txt) for (;;)
{
char c = *txt++;
switch (c)
{
case 0: return;
case '<': str->Append("&lt;"); break;
case '>': str->Append("&gt;"); break;
case '&': str->Append("&amp;"); break;
case ' ': str->Append(is_value ? " " : "_"); break;
default: str->Append(&c,1); break;
}
}
}
const char *XMLHasOpenTag(WDL_FastString *str, const char *tag) // tag like "<FOO>")
{
// stupid
int taglen=strlen(tag);
const char *open=strstr(str->Get(), tag);
while (open)
{
const char *close=strstr(open+taglen, tag+1);
if (!close || WDL_NOT_NORMALLY(close[-1] != '/')) break;
open=strstr(close+taglen-1, tag);
}
return open;
}
void UnpackXMLElement(const char *pre, wdl_xml_element *elem,
WDL_StringKeyedArray<char*> *metadata)
{
WDL_FastString key;
if (stricmp(elem->name, "BWFXML"))
{
key.SetFormatted(512, "%s:%s", pre, elem->name);
pre=key.Get();
}
if (elem->value.Get()[0])
{
const char *k=key.Get();
if (!strncmp(k, "IXML:ASWG:", 10))
{
k += 5;
}
else if (!strncmp(k, "IXML:BEXT:", 10))
{
// we could rewrite this as follows, but this might overwrite an actual bext chunk so maybe not?
/*
if (!strcmp(k+10, "BWF_DESCRIPTION")) k="BWF:Description";
else if (!strcmp(k+10, "BWF_ORIGINATOR")) k="BWF:Originator";
else if (!strcmp(k+10, "BWF_ORIGINATOR_REFERENCE")) k="BWF:OriginatorReference";
else if (!strcmp(k+10, "BWF_ORIGINATION_DATE")) k="BWF:OriginationDate";
else if (!strcmp(k+10, "BWF_ORIGINATION_TIME")) k="BWF:OriginationTime";
else if (!strcmp(k+10, "BWF_TIME_REFERENCE")) k="BWF:TimeReference"; // todo parse lo/hi
else if (!strcmp(k+10, "BWF_VERSION")) k="BWF:Version";
else if (!strcmp(k+10, "BWF_LOUDNESS_VALUE")) k="BWF:LoudnessValue";
else if (!strcmp(k+10, "BWF_LOUDNESS_RANGE")) k="BWF:LoudnessRange";
else if (!strcmp(k+10, "BWF_MAX_TRUE_PEAK_LEVEL")) k="BWF:MaxTruePeakLevel";
else if (!strcmp(k+10, "BWF_MAX_MOMENTARY_LOUDNESS")) k="BWF:MaxMomentaryLoudness";
else if (!strcmp(k+10, "BWF_MAX_SHORT_TERM_LOUDNESS")) k="BWF:MaxShortTermLoudness";
*/
}
InsertMetadataIncrKeyIfNeeded(metadata, k, elem->value.Get());
}
for (int i=0; i < elem->elements.GetSize(); ++i)
{
wdl_xml_element *elem2=elem->elements.Get(i);
UnpackXMLElement(pre, elem2, metadata);
}
}
bool UnpackIXMLChunk(const char *buf, int buflen,
WDL_StringKeyedArray<char*> *metadata)
{
if (!buf || !buflen || !metadata) return false;
while (buflen > 20 && strnicmp(buf, "<BWFXML>", 8))
{
++buf;
--buflen;
}
if (buflen >= 20)
{
wdl_xml_parser xml(buf, buflen);
if (!xml.parse() && xml.element_root)
{
UnpackXMLElement("IXML", xml.element_root, metadata);
return true;
}
}
return false;
}
bool IsXMPMetadata(const char *name, WDL_FastString *key)
{
if (!name || !name[0] || !key) return false;
// returns true if this XMP schema is one we know/care about
if (!strnicmp(name, "xmpDM:", 6) && name[6])
{
key->SetFormatted(512, "XMP:dm/%s", name+6);
return true;
}
if (!strnicmp(name, "dc:", 3) && name[3])
{
key->SetFormatted(512, "XMP:dc/%s", name+3);
return true;
}
return false;
}
double UnpackXMPTimestamp(wdl_xml_element *elem)
{
double tval=-1.0;
int num=0, denom=0;
for (int i=0; i < elem->attributes.GetSize(); ++i)
{
char *attr;
const char *val=elem->attributes.Enumerate(i, &attr);
if (!strcmp(attr, "xmpDM:scale") && val && val[0])
{
if (sscanf(val, "%d/%d", &num, &denom) != 2) num=denom=0;
}
else if (!strcmp(attr, "xmpDM:value") && val && val[0])
{
tval=atof(val);
}
}
if (tval >= 0.0 && num > 0 && denom > 0)
{
return tval*(double)num/(double)denom;
}
return -1.0;
}
const char *GetXMPSubElement(wdl_xml_element *elem, const char *name)
{
for (int i=0; i < elem->elements.GetSize(); ++i)
{
wdl_xml_element *elem2=elem->elements.Get(i);
if (!strcmp(elem2->name, name)) return elem2->value.Get(); // may be ""
}
return NULL; // element does not exist
}
bool IsXMPResourceList(wdl_xml_element *elem)
{
if (!strcmp(elem->name, "rdf:li"))
{
const char *val=elem->get_attribute("rdf:parseType");
if (val && !strcmp(val, "Resource")) return true;
}
return false;
}
// keep in sync with metadata.cpp:XMP_MARKER_RESOLUTION
#define XMP_MARKER_RESOLUTION 1000000
// todo generic PopulateCuesFromMetadata function metadata => list of REAPERCues
void UnpackXMPTrack(wdl_xml_element *elem, WDL_StringKeyedArray<char*> *metadata)
{
int num_markers=0;
WDL_FastString key, val;
for (int i=0; i < elem->elements.GetSize(); ++i)
{
wdl_xml_element *elem2=elem->elements.Get(i);
if (!strcmp(elem2->name, "rdf:Bag"))
{
for (int i2=0; i2 < elem2->elements.GetSize(); ++i2)
{
wdl_xml_element *elem3=elem2->elements.Get(i2);
if (IsXMPResourceList(elem3))
{
const char *track_type=GetXMPSubElement(elem3, "xmpDM:trackType");
const char *frame_rate=GetXMPSubElement(elem3, "xmpDM:frameRate");
if (track_type && frame_rate && !strcmp(track_type, "Cue"))
{
double marker_resolution=0;
if (frame_rate[0] == 'f') marker_resolution=atof(frame_rate+1);
// todo parse other resolution types
if (marker_resolution > 0.0)
{
double marker_convert=XMP_MARKER_RESOLUTION/marker_resolution;
for (int i3=0; i3 < elem3->elements.GetSize(); ++i3)
{
wdl_xml_element *elem4=elem3->elements.Get(i3);
if (!strcmp(elem4->name, "xmpDM:markers"))
{
for (int i4=0; i4 < elem4->elements.GetSize(); ++i4)
{
wdl_xml_element *elem5=elem4->elements.Get(i4);
if (!strcmp(elem5->name, "rdf:Seq"))
{
for (int i5=0; i5 < elem5->elements.GetSize(); ++i5)
{
wdl_xml_element *elem6=elem5->elements.Get(i4);
const char *start_time=GetXMPSubElement(elem6, "xmpDM:startTime");
const char *duration=GetXMPSubElement(elem6, "xmpDM:duration");
const char *marker_name=GetXMPSubElement(elem6, "xmpDM:name");
if (start_time)
{
double st=atof(start_time)*marker_convert;
double et = st + (duration ? atof(duration)*marker_convert : 0.0);
key.SetFormatted(512, "XMP:MARK%03d", num_markers++);
val.SetFormatted(512, "%.0f:%.0f:", st, et);
if (marker_name && marker_name[0]) val.Append(marker_name);
else val.AppendFormatted(512, "Marker %d", num_markers);
metadata->Insert(key.Get(), strdup(val.Get()));
}
}
}
}
}
}
}
}
}
}
}
}
}
void UnpackXMPDescription(const char *curkey, wdl_xml_element *elem,
WDL_StringKeyedArray<char*> *metadata)
{
if (!strcmp(elem->name, "xmpDM:Tracks"))
{
// xmp "tracks" are collections of markers and other related data,
// we can parse the markers (at least ones we wronte) but otherwise
// we can ignore this entire block
UnpackXMPTrack(elem, metadata);
return;
}
if (!strcmp(elem->name, "xmpDM:relativeTimestamp"))
{
double tval=UnpackXMPTimestamp(elem);
if (tval >= 0.0)
{
char buf[512];
snprintf(buf, sizeof(buf), "%.0f", floor(tval*1000.0));
metadata->Insert("XMP:dm/relativeTimestamp", strdup(buf));
}
return;
}
WDL_FastString key;
int i;
for (i=0; i < elem->attributes.GetSize(); ++i)
{
char *attr;
const char *val=elem->attributes.Enumerate(i, &attr);
if (IsXMPMetadata(attr, &key) && val && val[0])
{
metadata->Insert(key.Get(), strdup(val));
}
}
if (IsXMPMetadata(elem->name, &key)) curkey=key.Get();
if (curkey && elem->value.Get()[0])
{
InsertMetadataIncrKeyIfNeeded(metadata, curkey, elem->value.Get());
}
for (i=0; i < elem->elements.GetSize(); ++i)
{
wdl_xml_element *elem2=elem->elements.Get(i);
UnpackXMPDescription(curkey, elem2, metadata);
}
}
void UnpackXMPElement(wdl_xml_element *elem, WDL_StringKeyedArray<char*> *metadata)
{
if (!strcmp(elem->name, "rdf:Description"))
{
// everything we care about is in this block
UnpackXMPDescription(NULL, elem, metadata);
return;
}
for (int i=0; i < elem->elements.GetSize(); ++i)
{
wdl_xml_element *elem2=elem->elements.Get(i);
UnpackXMPElement(elem2, metadata);
}
}
bool UnpackXMPChunk(const char *buf, int buflen,
WDL_StringKeyedArray<char*> *metadata)
{
if (!buf || !buflen || !metadata) return false;
wdl_xml_parser xmp(buf, buflen);
if (!xmp.parse() && xmp.element_root)
{
UnpackXMPElement(xmp.element_root, metadata);
return true;
}
return false;
}
// metadata is passed as an assocarray of id=>value strings,
// where id has the form "scheme:identifier"
// example "ID3:TIT2", "INFO:IPRD", "VORBIS:ALBUM", etc
// for user-defined metadata, the form is extended to "scheme:identifier:key",
// example "ID3:TXXX:mykey", "VORBIS:USER:mykey", etc
// id passed to this function is just "identifier:key" part
// NOTE: WDL/lameencdec and WDL/vorbisencdec have copies of this, so edit them if this changes
bool ParseUserDefMetadata(const char *id, const char *val,
const char **k, const char **v, int *klen, int *vlen)
{
const char *sep=strchr(id, ':');
if (sep) // key encoded in id, version >= 6.12
{
*k=sep+1;
*klen=strlen(*k);
*v=val;
*vlen=strlen(*v);
return true;
}
sep=strchr(val, '=');
if (sep) // key encoded in value, version <= 6.11
{
*k=val;
*klen=sep-val;
*v=sep+1;
*vlen=strlen(*v);
return true;
}
// no key, version <= 6.11
*k="User";
*klen=strlen(*k);
*v=val;
*vlen=strlen(*v);
return false;
}
bool HasScheme(const char *scheme, WDL_StringKeyedArray<char*> *metadata)
{
if (!scheme || !scheme[0] || !metadata) return false;
bool ismatch=false;
int idx=metadata->LowerBound(scheme, &ismatch);
const char *key=NULL;
metadata->Enumerate(idx, &key);
if (key && !strnicmp(key, scheme, strlen(scheme))) return true;
return false;
}
WDL_INT64 _ATOI64(const char *str)
{
bool neg=false;
if (*str == '-') { neg=true; str++; }
WDL_INT64 v=0;
while (*str >= '0' && *str <= '9')
{
v = (v*10) + (WDL_INT64) (neg ? -(*str - '0') : (*str - '0'));
str++;
}
return v;
}
int PackIXMLChunk(WDL_HeapBuf *hb, WDL_StringKeyedArray<char*> *metadata, int padtolen)
{
if (!hb || !metadata) return 0;
if (!HasScheme("IXML", metadata) &&
!HasScheme("ASWG", metadata) &&
!HasScheme("BWF", metadata))
{
return 0;
}
int olen=hb->GetSize();
WDL_FastString ixml;
const char *ixml_open="<?xml version=\"1.0\" encoding=\"UTF-8\"?><BWFXML>";
const char *need_close=NULL;
int junklen=0;
for (int i=0; i < metadata->GetSize(); ++i)
{
const char *key;
const char *val=metadata->Enumerate(i, &key);
if (!key || !key[0] || !val || !val[0]) continue;
const char *sec =
!strncmp(key, "ASWG:", 5) ? "ASWG" :
!strncmp(key, "BWF:", 4) ? "BWF" :
!strncmp(key, "IXML:USER:", 10) ? "USER" :
!strncmp(key, "IXML:", 5) ? "IXML" :
NULL;
if (!sec) continue;
key += strlen(sec)+1;
if (!strcmp(sec, "BWF"))
{
if (!strcmp(key, "Description")) key="BWF_DESCRIPTION";
else if (!strcmp(key, "Originator")) key="BWF_ORIGINATOR";
else if (!strcmp(key, "OriginatorReference")) key="BWF_ORIGINATOR_REFERENCE";
else if (!strcmp(key, "OriginationDate")) key="BWF_ORIGINATION_DATE";
else if (!strcmp(key, "OriginationTime")) key="BWF_ORIGINATION_TIME";
else if (!strcmp(key, "TimeReference")) key="BWF_TIME_REFERENCE";
else if (!strcmp(key, "Version")) key="BWF_VERSION";
else if (!strcmp(key, "LoudnessValue")) key="BWF_LOUDNESS_VALUE";
else if (!strcmp(key, "LoudnessRange")) key="BWF_LOUDNESS_RANGE";
else if (!strcmp(key, "MaxTruePeakLevel")) key="BWF_MAX_TRUE_PEAK_LEVEL";
else if (!strcmp(key, "MaxMomentaryLoudness")) key="BWF_MAX_MOMENTARY_LOUDNESS";
else if (!strcmp(key, "MaxShortTermLoudness")) key="BWF_MAX_SHORT_TERM_LOUDNESS";
else continue;
}
if (!ixml.GetLength()) ixml.Append(ixml_open);
if (need_close && strcmp(need_close, sec))
{
ixml.AppendFormatted(512, "</%s>", need_close);
need_close=NULL;
}
if (!need_close && strcmp(sec, "IXML"))
{
ixml.AppendFormatted(512, "<%s>", sec);
need_close=sec;
}
if (!strcmp(key, "BWF_TIME_REFERENCE"))
{
WDL_UINT64 pos=_ATOI64(val);
int hi=pos>>32, lo=(pos&0xFFFFFFFF);
ixml.AppendFormatted(4096, "<%s_HIGH>%d</%s_HIGH>", key, hi, key);
ixml.AppendFormatted(4096, "<%s_LOW>%d</%s_LOW>", key, lo, key);
continue;
}
if (!strcmp(sec, "USER"))
{
const char *k, *v;
int klen, vlen;
ParseUserDefMetadata(key, val, &k, &v, &klen, &vlen);
key=k;
val=v;
}
if (!strncmp(val, "#junk#", 6))
{
junklen += 11+2*strlen(key)+strlen(val);
continue;
}
ixml.Append("<");
XMLCompliantAppend(&ixml, key, false);
ixml.Append(">");
XMLCompliantAppend(&ixml, val, true);
ixml.Append("</");
XMLCompliantAppend(&ixml, key, false);
ixml.Append(">");
}
if (need_close)
{
ixml.AppendFormatted(512, "</%s>", need_close);
}
if (ixml.GetLength())
{
ixml.Append("</BWFXML>");
int ixmllen=ixml.GetLength();
int len=ixmllen+1+junklen;
if (len < padtolen) len=padtolen;
if (len&1) ++len;
unsigned char *p=(unsigned char*)hb->Resize(olen+len);
if (p)
{
memcpy(p+olen, ixml.Get(), ixmllen);
memset(p+olen+ixmllen, 0, len-ixmllen);
}
}
return hb->GetSize()-olen;
}
int PackXMPChunk(WDL_HeapBuf *hb, WDL_StringKeyedArray<char*> *metadata)
{
if (!hb || !metadata) return 0;
if (!HasScheme("XMP", metadata)) return 0;
int olen=hb->GetSize();
static const char *xmp_hdr=
"<?xpacket begin=\"\xEF\xBB\xBF\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>"
"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">"
"<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">"
"<rdf:Description"
" xmlns:dc=\"http://purl.org/dc/elements/1.1/\""
" xmlns:xmpDM=\"http://ns.adobe.com/xmp/1.0/DynamicMedia/\""; // unclosed
static const char *xmp_ftr=
"</rdf:Description>"
"</rdf:RDF>"
"</x:xmpmeta>"
"<?xpacket end=\"w\"?>";
WDL_FastString xmp(xmp_hdr);
for (int pass=0; pass < 2; ++pass) // attributes, then elements
{
if (pass) xmp.Append(">");
for (int i=0; i < metadata->GetSize(); ++i)
{
const char *key;
const char *val=metadata->Enumerate(i, &key);
if (!key || !key[0] || !val || !val[0]) continue;
if (!strncmp(key, "XMP:", 4) && key[4])
{
key += 4;
const char *prefix = // xmp schema
!strncmp(key, "dc/", 3) ? "dc" :
!strncmp(key, "dm/", 3) ? "xmpDM" : NULL;
if (prefix && key[3])
{
if (!strcmp(key, "dm/markers")) continue;
if (!strcmp(key, "dc/description") || !strcmp(key, "dc/title"))
{
// elements
if (!pass) continue;
key += 3;
const char *lang=metadata->Get("XMP:dc/language");
if (!lang) lang="x-default";
xmp.AppendFormatted(1024, "<%s:%s>", prefix, key);
xmp.AppendFormatted(1024, "<rdf:Alt><rdf:li xml:lang=\"%s\">", lang);
xmp.Append(val);
xmp.Append("</rdf:li></rdf:Alt>");
xmp.AppendFormatted(1024, "</%s:%s>", prefix, key);
}
else if (!strcmp(key, "dm/relativeTimestamp"))
{
// element
if (!pass) continue;
key += 3;
xmp.AppendFormatted(1024, "<%s:%s xmpDM:value=\"%s\" xmpDM:scale=\"1/1000\"/>",
prefix, key, val);
}
else
{
// attributes
if (pass) continue;
key += 3;
xmp.AppendFormatted(1024, " %s:%s=\"%s\"", prefix, key, val);
}
}
}
}
}
static const char *track_hdr1=
"<xmpDM:Tracks>"
"<rdf:Bag>"
"<rdf:li rdf:parseType=\"Resource\">"
"<xmpDM:trackType>Cue</xmpDM:trackType>"
"<xmpDM:frameRate>f";
static const char *track_hdr2=
"</xmpDM:frameRate>"
"<xmpDM:markers>"
"<rdf:Seq>";
static const char *track_ftr=
"</rdf:Seq>"
"</xmpDM:markers>"
"</rdf:li>"
"</rdf:Bag>"
"</xmpDM:Tracks>";
char buf[128];
int cnt=0;
for (int i=0; i < 1000; ++i)
{
snprintf(buf, sizeof(buf), "XMP:MARK%03d", i);
const char *val=metadata->Get(buf);
if (!val || !val[0]) break;
const char *sep1=strchr(val, ':');
if (WDL_NOT_NORMALLY(!sep1)) break;
double st=atof(val);
double et=atof(sep1+1);
if (WDL_NOT_NORMALLY(st < 0.0 || et < st)) break;
const char *sep2=strchr(sep1+1, ':');
const char *name = sep2 ? sep2+1 : ""; // might need to make this xml compliant?
if (!cnt++) xmp.AppendFormatted(1024, "%s%d%s", track_hdr1, XMP_MARKER_RESOLUTION, track_hdr2);
xmp.Append("<rdf:li rdf:parseType=\"Resource\">");
xmp.AppendFormatted(1024, "<xmpDM:startTime>%.0f</xmpDM:startTime>", st);
if (et > st) xmp.AppendFormatted(1024, "<xmpDM:duration>%.0f</xmpDM:duration>", et-st);
if (name[0]) xmp.AppendFormatted(1024, "<xmpDM:name>%s</xmpDM:name>", name);
xmp.Append("</rdf:li>");
}
if (cnt) xmp.Append(track_ftr);
xmp.Append(xmp_ftr);
int xmplen=xmp.GetLength();
int len=xmplen+1;
if (len&1) ++len;
unsigned char *p=(unsigned char*)hb->Resize(olen+len);
if (p)
{
memcpy(p+olen, xmp.Get(), xmplen);
memset(p+olen+xmplen, 0, len-xmplen);
}
return hb->GetSize()-olen;
}
int PackVorbisFrame(WDL_HeapBuf *hb, WDL_StringKeyedArray<char*> *metadata, bool for_vorbis)
{
if (!hb || !metadata) return 0;
// for vorbis, we need an empty frame even if there's no metadata
if (!for_vorbis && !HasScheme("VORBIS", metadata)) return 0;
int olen=hb->GetSize();
const char *vendor="REAPER";
const int vendorlen=strlen(vendor);
int framelen=4+vendorlen+4+for_vorbis;
int i, tagcnt=0;
for (i=0; i < metadata->GetSize(); ++i)
{
const char *key;
const char *val=metadata->Enumerate(i, &key);
if (!key || !key[0] || !val || !val[0]) continue;
if (!strncmp(key, "VORBIS:", 7) && key[7])
{
key += 7;
const char *k=key, *v=val;
int klen=strlen(k), vlen=strlen(v);
if (!strncmp(key, "USER", 4))
{
ParseUserDefMetadata(key, val, &k, &v, &klen, &vlen);
}
int taglen=4+klen+1+vlen;
if (framelen+taglen >= 0xFFFFFF) break;
framelen += taglen;
++tagcnt;
}
}
unsigned char *buf=(unsigned char*)hb->Resize(olen+framelen)+olen;
if (buf)
{
unsigned char *p=buf;
memcpy(p, &vendorlen, 4);
p += 4;
memcpy(p, vendor, vendorlen);
p += vendorlen;
memcpy(p, &tagcnt, 4);
p += 4;
for (i=0; i < metadata->GetSize(); ++i)
{
const char *key;
const char *val=metadata->Enumerate(i, &key);
if (!key || !key[0] || !val || !val[0]) continue;
if (!strncmp(key, "VORBIS:", 7) && key[7])
{
key += 7;
const char *k=key, *v=val;
int klen=strlen(k), vlen=strlen(v);
if (!strncmp(key, "USER", 4))
{
ParseUserDefMetadata(key, val, &k, &v, &klen, &vlen);
}
int taglen=klen+1+vlen;
memcpy(p, &taglen, 4);
p += 4;
while (*k)
{
*p++ = (*k >= ' ' && *k <= '}' && *k != '=') ? *k : ' ';
k++;
}
*p++='=';
memcpy(p, v, vlen);
p += vlen;
if (!--tagcnt) break;
}
}
if (for_vorbis) *p++=1; // framing bit
if (WDL_NOT_NORMALLY(p-buf != framelen) || framelen > 0xFFFFFF)
{
hb->Resize(olen);
}
}
return hb->GetSize()-olen;
}
bool UnpackVorbisFrame(unsigned char *frame, int framelen,
WDL_StringKeyedArray<char*> *metadata)
{
if (!frame || !framelen || !metadata) return 0;
char *p=(char*)frame;
int vendor_len=*(int*)p;
if (4+vendor_len+4 > framelen) return false;
p += 4+vendor_len;
int tagcnt=*(int*)p;
p += 4;
WDL_String str;
int pos=4+vendor_len+4;
while (pos < framelen && tagcnt--)
{
int taglen=*(int*)p;
p += 4;
if (pos+taglen > framelen) return false;
str.Set("VORBIS:");
str.Append(p, taglen);
p += taglen;
const char *sep=strchr(str.Get(), '=');
if (!sep) return false;
*(char*)sep=0;
metadata->Insert(str.Get(), strdup(sep+1));
}
return (pos == framelen && tagcnt == 0);
}
#define _AddInt32LE(i) \
*p++=((i)&0xFF); \
*p++=(((i)>>8)&0xFF); \
*p++=(((i)>>16)&0xFF); \
*p++=(((i)>>24)&0xFF);
#define _GetInt32LE(p) \
(((p)[0])|((p)[1]<<8)|((p)[2]<<16)|((p)[3]<<24))
int PackApeChunk(WDL_HeapBuf *hb, WDL_StringKeyedArray<char*> *metadata)
{
if (!hb || !metadata) return false;
if (!HasScheme("APE", metadata)) return false;
int olen=hb->GetSize();
int i, apelen=0, cnt=0;
for (i=0; i < metadata->GetSize(); ++i)
{
const char *key;
const char *val=metadata->Enumerate(i, &key);
if (strlen(key) < 5 || strncmp(key, "APE:", 4) || !val || !val[0]) continue;
key += 4;
if (!apelen) apelen=64; // includes header and footer
if (!strncmp(key, "User Defined", 12))
{
const char *k, *v;
int klen, vlen;
ParseUserDefMetadata(key, val, &k, &v, &klen, &vlen);
apelen += 8+klen+1+vlen;
}
else
{
apelen += 8+strlen(key)+1+strlen(val);
}
++cnt;
}
if (!apelen) return false;
unsigned char *buf=(unsigned char*)hb->Resize(olen+apelen)+olen;
if (buf)
{
unsigned char *p=buf;
memcpy(p, "APETAGEX", 8);
p += 8;
_AddInt32LE(2000); // version
_AddInt32LE(apelen-32); // includes footer but not header
_AddInt32LE(cnt);
_AddInt32LE((1<<31)|(1<<30)|(1<<29)); // tag contains header and footer, this is the header
_AddInt32LE(0);
_AddInt32LE(0);
for (i=0; i < metadata->GetSize(); ++i)
{
const char *key;
const char *val=metadata->Enumerate(i, &key);
if (strlen(key) < 5 || strncmp(key, "APE:", 4) || !val || !val[0]) continue;
key += 4;
const char *k=key, *v=val;
int klen, vlen;
if (!strncmp(key, "User Defined", 12))
{
ParseUserDefMetadata(key, val, &k, &v, &klen, &vlen);
}
else
{
klen=strlen(k);
vlen=strlen(v);
}
_AddInt32LE(vlen);
_AddInt32LE(0);
while (klen--)
{
*p = (*k >= 0x20 && *k <= 0x7E ? *k : ' ');
++p;
++k;
}
*p++=0;
memcpy(p, v, vlen);
p += vlen;
}
memcpy(p, "APETAGEX", 8);
p += 8;
_AddInt32LE(2000); // version
_AddInt32LE(apelen-32); // includes footer but not header
_AddInt32LE(cnt);
_AddInt32LE((1<<31)|(1<<30)|(1<<28)); // tag contains header and footer, this is the footer
_AddInt32LE(0);
_AddInt32LE(0);
if (WDL_NOT_NORMALLY(p-buf != apelen)) hb->Resize(olen);
}
return hb->GetSize()-olen;
}
const char *EnumMetadataSchemeFromFileType(const char *filetype, int idx)
{
if (!filetype || !filetype[0]) return NULL;
if (filetype[0] == '.') ++filetype;
if (!stricmp(filetype, "bwf")) filetype="wav";
else if (!stricmp(filetype, "opus")) filetype="ogg";
else if (!stricmp(filetype, "aiff")) filetype="aif";
else if (!stricmp(filetype, "caff")) filetype="caf";
static const char *WAV_SCHEMES[]=
{
"BWF", "INFO", "IXML", "ASWG", "XMP", "AXML", "CART", "ID3",
};
static const char *MP3_SCHEMES[]=
{
"ID3", "APE", "IXML", "ASWG", "XMP",
};
static const char *FLAC_SCHEMES[]=
{
"VORBIS", "BWF", "IXML", "ASWG", "XMP",
};
static const char *OGG_SCHEMES[]=
{
"VORBIS",
};
static const char *WV_SCHEMES[]=
{
"BWF", "APE",
};
static const char *AIF_SCHEMES[]=
{
"IFF", "XMP", "ID3"
};
static const char *CAF_SCHEMES[]=
{
"CAFINFO",
};
static const char *RX2_SCHEMES[]=
{
"REX",
};
#define DO_SCHEME_MAP(X) if (!stricmp(filetype, #X)) \
return idx < sizeof(X##_SCHEMES)/sizeof(X##_SCHEMES[0]) ? X##_SCHEMES[idx] : NULL;
DO_SCHEME_MAP(WAV);
DO_SCHEME_MAP(MP3);
DO_SCHEME_MAP(FLAC);
DO_SCHEME_MAP(OGG);
DO_SCHEME_MAP(WV);
DO_SCHEME_MAP(AIF);
DO_SCHEME_MAP(CAF);
DO_SCHEME_MAP(RX2);
#undef DO_SCHEME_MAP
return NULL;
}
bool EnumMetadataKeyFromMexKey(const char *mexkey, int idx, char *key, int keylen)
{
if (!mexkey || !mexkey[0] || idx < 0 || !key || !keylen) return false;
// TO_DO_IF_METADATA_UPDATE
// "TITLE", "ARTIST", "ALBUM", "YEAR", "GENRE", "COMMENT", "DESC", "BPM", "KEY", "DB_CUSTOM", "TRACKNUMBER"
if (!strcmp(mexkey, "DATE")) mexkey="YEAR";
// callers handle PREFPOS
// general priority order here:
// BWF
// INFO
// ID3
// APE
// VORBIS
// CART
// IXML
// ASWG
// XMP
// CAFINFO
// IFF
// REX
static const char *TITLE_KEYS[]=
{
"INFO:INAM",
"ID3:TIT2",
"APE:Title",
"VORBIS:TITLE",
"CART:Title",
"IXML:PROJECT",
"ASWG:project",
"XMP:dc/title",
"CAFINFO:title",
"IFF:NAME",
"REX:Name",
};
static const char *ARTIST_KEYS[]=
{
"INFO:IART",
"ID3:TPE1",
"APE:Artist",
"VORBIS:ARTIST",
"CART:Artist",
"XMP:dm/artist",
"CAFINFO:artist",
"IFF:AUTH",
};
static const char *ALBUM_KEYS[]=
{
"INFO:IALB",
"INFO:IPRD",
"ID3:TALB",
"APE:Album",
"VORBIS:ALBUM",
"XMP:dm/album",
"CAFINFO:album",
};
static const char *YEAR_KEYS[]= // really DATE
{
"BWF:OriginationDate",
"INFO:ICRD",
"ID3:TYER",
"ID3:TDRC",
"APE:Year",
"APE:Record Date",
"VORBIS:DATE",
"CART:StartDate",
"XMP:dc/date",
"CAFINFO:year",
};
static const char *GENRE_KEYS[]=
{
"INFO:IGNR",
"ID3:TCON",
"APE:Genre",
"VORBIS:GENRE",
"CART:Category",
"XMP:dm/genre",
"CAFINFO:genre",
};
static const char *COMMENT_KEYS[]=
{
"INFO:ICMT",
"ID3:COMM",
"APE:Comment",
"VORBIS:COMMENT",
"CART:TagText",
"IXML:NOTE",
"ASWG:notes",
"XMP:dm/logComment",
"CAFINFO:comments",
"CAFINFO:comment", // spec is "comments" but ffmpeg may write "comment"
"REX:FreeText",
};
static const char *DESC_KEYS[]=
{
"BWF:Description",
"INFO:ISBJ",
"INFO:IKEY",
"ID3:TIT3",
"APE:Subtitle",
"VORBIS:DESCRIPTION",
"XMP:dc/description",
"IFF:ANNO",
};
static const char *BPM_KEYS[]=
{
"ACID:BPM",
"ID3:TBPM",
"APE:BPM",
"VORBIS:BPM",
"XMP:dm/tempo",
"CAFINFO:tempo",
};
static const char *KEY_KEYS[]=
{
"ACID:KEY",
"ID3:TKEY",
"APE:Key",
"VORBIS:KEY",
"XMP:dm/key",
"CAFINFO:key signature",
};
static const char *TRACKNUMBER_KEYS[]=
{
"INFO:TRCK",
"ID3:TRCK",
"APE:Track",
"VORBIS:TRACKNUMBER",
"CART:CutID",
// "IXML:TRACK",
"XMP:dm/trackNumber",
"CAFINFO:track number",
};
#define DO_MEXKEY_MAP(K) \
if (!strcmp(mexkey, #K)) \
{ \
if (idx >= sizeof(K##_KEYS)/sizeof(K##_KEYS[0])) return false; \
lstrcpyn(key, K##_KEYS[idx], keylen); return true; \
}
key[0]=0;
DO_MEXKEY_MAP(TITLE);
DO_MEXKEY_MAP(ARTIST);
DO_MEXKEY_MAP(ALBUM);
DO_MEXKEY_MAP(TRACKNUMBER);
DO_MEXKEY_MAP(YEAR);
DO_MEXKEY_MAP(GENRE);
DO_MEXKEY_MAP(COMMENT);
DO_MEXKEY_MAP(DESC);
DO_MEXKEY_MAP(BPM);
DO_MEXKEY_MAP(KEY);
#undef DO_MEXKEY_MAP
static const char *DB_CUSTOM_KEYS[]=
{
"ID3:TXXX",
"APE",
"VORBIS",
"IXML:USER",
};
if (idx >= sizeof(DB_CUSTOM_KEYS)/sizeof(DB_CUSTOM_KEYS[0])) return false;
if (!strcmp(mexkey, "DB_CUSTOM")) mexkey="REAPER";
snprintf(key, keylen, "%s:%s", DB_CUSTOM_KEYS[idx], mexkey);
return true;
}
const char *GetMexKeyFromMetadataKey(const char *key)
{
int i=0;
const char *mexkey;
while ((mexkey=EnumMexKeys(i++)))
{
int j=0;
char tkey[256];
while (EnumMetadataKeyFromMexKey(mexkey, j++, tkey, sizeof(tkey)) && tkey[0])
{
if (!strcmp(key, tkey)) return mexkey;
}
}
return NULL;
}
bool HandleMexMetadataRequest(const char *mexkey, char *buf, int buflen,
WDL_StringKeyedArray<char*> *metadata)
{
if (!mexkey || !mexkey[0] || !buf || !buflen || !metadata) return false;
buf[0]=0;
buf[0]=0;
int i=0;
char key[256];
while (EnumMetadataKeyFromMexKey(mexkey, i++, key, sizeof(key)) && key[0])
{
const char *val=metadata->Get(key);
if (val && val[0])
{
lstrcpyn(buf, val, buflen);
return true;
}
}
if (strchr(mexkey, ':'))
{
const char *val=metadata->Get(mexkey);
if (val && val[0])
{
lstrcpyn(buf, val, buflen);
return true;
}
}
return false;
}
void WriteMetadataPrefPos(double prefpos, int srate, // prefpos <= 0.0 to clear
WDL_StringKeyedArray<char*> *metadata)
{
if (!metadata) return;
metadata->Delete("BWF:TimeReference");
metadata->Delete("ID3:TXXX:TIME_REFERENCE");
metadata->Delete("IXML:BEXT:BWF_TIME_REFERENCE_HIGH");
metadata->Delete("IXML:BEXT:BWF_TIME_REFERENCE_LOW");
metadata->Delete("XMP:dm/relativeTimestamp");
metadata->Delete("VORBIS:TIME_REFERENCE");
if (prefpos > 0.0 && srate > 1)
{
char buf[128];
if (srate > 0.0)
{
snprintf(buf, sizeof(buf), "%.0f", floor(prefpos*(double)srate));
metadata->Insert("BWF:TimeReference", strdup(buf));
// BWF:TimeReference causes IXML:BEXT element to be written as well
metadata->Insert("ID3:TXXX:TIME_REFERENCE", strdup(buf));
metadata->Insert("VORBIS:TIME_REFERENCE", strdup(buf));
}
snprintf(buf, sizeof(buf), "%.0f", floor(prefpos*1000.0));
metadata->Insert("XMP:dm/relativeTimestamp", strdup(buf));
}
}
void AddMexMetadata(WDL_StringKeyedArray<char*> *mex_metadata,
WDL_StringKeyedArray<char*> *metadata, int srate)
{
if (!mex_metadata || !metadata) return;
for (int idx=0; idx < mex_metadata->GetSize(); ++idx)
{
const char *mexkey;
const char *val=mex_metadata->Enumerate(idx, &mexkey);
if (!strcmp(mexkey, "PREFPOS"))
{
WDL_UINT64 ms = val && val[0] ? ParseUInt64(val) : 0;
WriteMetadataPrefPos((double)ms/1000.0, srate, metadata);
// caller may still have to do stuff if prefpos is represented
// in some other way outside the metadata we handle, like wavpack
continue;
}
int i=0;
char key[256];
while (EnumMetadataKeyFromMexKey(mexkey, i++, key, sizeof(key)) && key[0])
{
if (val && val[0]) metadata->Insert(key, strdup(val));
else metadata->Delete(key);
}
}
}
void DumpMetadata(WDL_FastString *str, WDL_StringKeyedArray<char*> *metadata)
{
if (!str || !metadata || !metadata->GetSize()) return;
char scheme[256];
scheme[0]=0;
char buf[2048];
int j=0;
const char *mexkey, *mexdesc;
while ((mexkey=EnumMexKeys(j++, &mexdesc)))
{
if (HandleMexMetadataRequest(mexkey, buf, sizeof(buf), metadata))
{
if (!scheme[0])
{
lstrcpyn(scheme, "mex", sizeof(scheme));
str->Append("Metadata:\r\n");
}
str->AppendFormatted(4096, " %s:%s%s\r\n",
mexdesc, strchr(buf, '\n') ? "\r\n" : " ", buf);
}
}
for (int i=0; i < metadata->GetSize(); ++i)
{
const char *key;
const char *val=metadata->Enumerate(i, &key);
if (!key || !key[0] || !val || !val[0] || !strncmp(val, "[Binary data]", 13)) continue;
const char *sep=strchr(key, ':');
if (sep)
{
int slen=wdl_min(sep-key, sizeof(scheme)-1);
if (strncmp(scheme, key, slen))
{
lstrcpyn(scheme, key, slen+1);
str->AppendFormatted(256, "%s tags:\r\n", scheme);
}
key += slen+1;
}
str->AppendFormatted(4096, " %s:%s%s\r\n",
key, strchr(val, '\n') ? "\r\n" : " ", val);
}
int unk_cnt=0;
for (int i=0; i < metadata->GetSize(); ++i)
{
const char *key;
const char *val=metadata->Enumerate(i, &key);
if (key && key[0] && val && (!val[0] || !strncmp(val, "[Binary data]", 13)))
{
if (!unk_cnt++) str->Append("Other file sections:\r\n");
str->AppendFormatted(4096, " %s%s\r\n", key,
!strnicmp(key, "smed", 4) ?
" (proprietary Soundminer metadata)" : "");
}
}
}
void CopyMetadata(WDL_StringKeyedArray<char*> *src, WDL_StringKeyedArray<char*> *dest)
{
if (!dest || !src) return;
dest->DeleteAll(false);
for (int i=0; i < src->GetSize(); ++i)
{
const char *key;
const char *val=src->Enumerate(i, &key);
dest->AddUnsorted(key, strdup(val));
}
dest->Resort(); // safe in case src/dest have diff sort attributes
}
bool CopyFileData(WDL_FileRead *fr, WDL_FileWrite *fw, WDL_INT64 len)
{
while (len)
{
char tmp[32768];
const int amt = (int) (wdl_min(len,sizeof(tmp)));
const int rd = fr->Read(tmp, amt);
if (rd != amt) return false;
if (fw->Write(tmp, rd) != rd) return false;
len -= rd;
}
return true;
}
bool EnumVorbisChapters(WDL_StringKeyedArray<char*> *metadata, int idx,
double *pos, const char **name)
{
if (!metadata) return false;
int cnt=0;
bool ismatch=false;
const char *prev=NULL;
int i=metadata->LowerBound("VORBIS:CHAPTER", &ismatch);
for (; i < metadata->GetSize(); ++i)
{
const char *key;
const char *val=metadata->Enumerate(i, &key);
if (strncmp(key, "VORBIS:CHAPTER", 14)) return false;
if (!prev || strncmp(key, prev, 17))
{
prev=key;
if (idx == cnt)
{
if (!key[17] && val && val[0])
{
// VORBIS:CHAPTER001 => 00:00:00.000
if (pos)
{
int hh=0, mm=0, ss=0, ms=0;
if (sscanf(val, "%d:%d:%d.%d", &hh, &mm, &ss, &ms) == 4)
{
*pos=(double)hh*3600.0+(double)mm*60.0+(double)ss+(double)ms*0.001;
}
}
if (name)
{
val=metadata->Enumerate(i+1, &key);
if (!strncmp(key, prev, 17) && !strcmp(key+17, "NAME"))
{
// VORBIS:CHAPTER001NAME => chapter name
*name=val;
}
}
return true;
}
return false;
}
++cnt;
}
}
return false;
}
#define _AddSyncSafeInt32(i) \
*p++=(((i)>>21)&0x7F); \
*p++=(((i)>>14)&0x7F); \
*p++=(((i)>>7)&0x7F); \
*p++=((i)&0x7F);
#define _AddInt32(i) \
*p++=(((i)>>24)&0xFF); \
*p++=(((i)>>16)&0xFF); \
*p++=(((i)>>8)&0xFF); \
*p++=((i)&0xFF);
#define _GetSyncSafeInt32(p) \
(((p)[0]<<21)|((p)[1]<<14)|((p)[2]<<7)|((p)[3]))
void WriteSyncSafeInt32(WDL_FileWrite *fw, int i)
{
unsigned char buf[4];
unsigned char *p=buf;
_AddSyncSafeInt32(i);
fw->Write(buf, 4);
}
#define CTOC_NAME "TOC" // arbitrary name of table of contents element
static bool _isnum(const char *v, int pos, int len)
{
for (int i=pos; i < pos+len; ++i)
{
if (v[i] < '0' || v[i] > '9') return false;
}
return true;
}
int IsID3TimeVal(const char *v)
{
if (strlen(v) == 4 && _isnum(v, 0, 4)) return 1;
if (strlen(v) == 5 && _isnum(v, 0, 2) && _isnum(v, 3, 2)) return 2;
return 0;
}
struct ID3RawTag
{
char key[8]; // we only use 4 chars + nul
WDL_HeapBuf val; // includes everything after taglen (flags, etc)
};
int ReadID3Raw(WDL_FileRead *fr, WDL_PtrList<ID3RawTag> *rawtags)
{
if (!fr || !fr->IsOpen() || !rawtags) return 0;
unsigned char buf[16];
if (fr->Read(buf, 10) != 10) return 0;
if (memcmp(buf, "ID3\x04", 4) && memcmp(buf, "ID3\x03", 4)) return 0;
int id3len=_GetSyncSafeInt32(buf+6);
if (!id3len) return 0;
int rdlen=0;
WDL_HeapBuf hb;
while (rdlen < id3len)
{
if (fr->Read(buf, 8) != 8) return 0;
if (!buf[0]) return 10+id3len; // padding
if ((buf[0] < 'A' || buf[0] > 'Z') && (buf[0] < '0' || buf[0] > '9')) return 0; // unexpected
int taglen=_GetSyncSafeInt32(buf+4)+2; // include flags in taglen
unsigned char *p=(unsigned char*)hb.ResizeOK(taglen);
if (!p || fr->Read(p, taglen) != taglen) return 0;
ID3RawTag *rawtag=rawtags->Add(new ID3RawTag);
memcpy(rawtag->key, buf, 4);
rawtag->key[4]=0;
rawtag->val=hb;
rdlen += 8+taglen;
if (rdlen == id3len) return 10+id3len;
}
return 0;
}
void DeleteID3Raw(WDL_PtrList<ID3RawTag> *rawtags, const char *key)
{
if (!rawtags || !rawtags->GetSize()) return;
if (strncmp(key, "ID3:", 4)) return;
if (WDL_NOT_NORMALLY(strlen(key) < 8)) return;
key += 4;
const char *subkey=NULL;
int suboffs=0, sublen=0;
if (key[4])
{
if (!strncmp(key, "TXXX:", 5)) suboffs=3;
else if (!strncmp(key, "PRIV:", 5)) suboffs=2;
if (!suboffs || !key[5]) return;
subkey=key+5;
sublen=strlen(subkey);
}
for (int i=0; i < rawtags->GetSize(); ++i)
{
ID3RawTag *rawtag=rawtags->Get(i);
if (!strncmp(key, rawtag->key, 4))
{
if (subkey &&
(rawtag->val.GetSize() < sublen+suboffs ||
memcmp((unsigned char*)rawtag->val.Get()+suboffs, subkey, sublen)))
{
continue; // key is like ID3:AAAA:BBBB but rawtag->val does not match *BBBB
}
rawtags->Delete(i--, true);
}
}
}
int PackID3Chunk(WDL_HeapBuf *hb, WDL_StringKeyedArray<char*> *metadata,
bool want_embed_otherschemes, int *ixml_lenwritten, int ixml_padtolen,
WDL_PtrList<ID3RawTag> *rawtags)
{
if (!hb || !metadata) return false;
bool want_ixml = want_embed_otherschemes &&
(HasScheme("IXML", metadata) || HasScheme("ASWG", metadata) || HasScheme("BWF", metadata));
bool want_xmp = want_embed_otherschemes && HasScheme("XMP", metadata);
if (!HasScheme("ID3", metadata) && !want_ixml && !want_xmp) return false;
int olen=hb->GetSize();
int id3len=0, chapcnt=0;
WDL_TypedQueue<char> toc;
int i;
for (i=0; i < metadata->GetSize(); ++i)
{
const char *key;
const char *val=metadata->Enumerate(i, &key);
if (strlen(key) < 8 || strncmp(key, "ID3:", 4) || !val) continue;
key += 4;
if (!strncmp(key, "TXXX", 4))
{
const char *k, *v;
int klen, vlen;
ParseUserDefMetadata(key, val, &k, &v, &klen, &vlen);
id3len += 10+1+klen+1+vlen;
}
else if (!strncmp(key, "TIME", 4))
{
if (IsID3TimeVal(val)) id3len += 10+1+4;
}
else if (key[0] == 'T' && strlen(key) == 4)
{
id3len += 10+1+strlen(val);
}
else if (!strcmp(key, "COMM") || !strcmp(key, "USLT"))
{
id3len += 10+5+strlen(val);
}
else if (!strncmp(key, "CHAP", 4) && chapcnt < 255)
{
const char *c1=strchr(val, ':');
const char *c2 = c1 ? strchr(c1+1, ':') : NULL;
if (c1)
{
++chapcnt;
const char *toc_entry=key; // use "CHAP001", etc as the internal toc entry
const char *chap_name = c2 ? c2+1 : NULL;
toc.Add(toc_entry, strlen(toc_entry)+1);
id3len += 10+strlen(toc_entry)+1+16;
if (chap_name) id3len += 10+1+strlen(chap_name)+1;
}
}
}
if (chapcnt)
{
id3len += 10+strlen(CTOC_NAME)+1+2+toc.GetSize();
}
WDL_HeapBuf apic_hdr;
int apic_datalen=0;
const char *apic_fn=metadata->Get("ID3:APIC_FILE");
if (apic_fn && apic_fn[0])
{
const char *mime=NULL;
const char *ext=WDL_get_fileext(apic_fn);
if (ext && (!stricmp(ext, ".jpg") || !stricmp(ext, ".jpeg"))) mime="image/jpeg";
else if (ext && !stricmp(ext, ".png")) mime="image/png";
if (mime)
{
FILE *fp=fopenUTF8(apic_fn, "rb"); // could stat but let's make sure we can open the file
if (fp)
{
fseek(fp, 0, SEEK_END);
apic_datalen=ftell(fp);
fclose(fp);
}
}
if (apic_datalen)
{
const char *t=metadata->Get("ID3:APIC_TYPE");
int type=-1;
if (t && t[0] >= '0' && t[0] <= '9') type=atoi(t);
if (type < 0 || type >= 16) type=3; // default "Cover (front)"
const char *desc=metadata->Get("ID3:APIC_DESC");
if (!desc) desc="";
int desclen=wdl_min(strlen(desc), 63);
int apic_hdrlen=1+strlen(mime)+1+1+desclen+1;
char *p=(char*)apic_hdr.Resize(apic_hdrlen);
if (p)
{
*p++=3; // UTF-8
memcpy(p, mime, strlen(mime)+1);
p += strlen(mime)+1;
*p++=type;
memcpy(p, desc, desclen);
p += desclen;
*p++=0;
id3len += 10+apic_hdrlen+apic_datalen;
}
}
}
WDL_HeapBuf ixml;
if (want_ixml)
{
PackIXMLChunk(&ixml, metadata, ixml_padtolen);
if (ixml.GetSize())
{
if (ixml_lenwritten) *ixml_lenwritten=ixml.GetSize();
id3len += 10+5+ixml.GetSize();
}
}
WDL_HeapBuf xmp;
if (want_xmp)
{
PackXMPChunk(&xmp, metadata);
if (xmp.GetSize()) id3len += 10+4+xmp.GetSize();
}
if (rawtags)
{
for (int i=0; i < rawtags->GetSize(); ++i)
{
ID3RawTag *rawtag=rawtags->Get(i);
if (WDL_NORMALLY(rawtag && rawtag->key[0] && rawtag->val.GetSize()))
{
id3len += 8+rawtag->val.GetSize();
}
}
}
if (id3len)
{
id3len += 10;
unsigned char *buf=(unsigned char*)hb->Resize(olen+id3len)+olen;
if (buf)
{
chapcnt=0;
unsigned char *p=buf;
memcpy(p,"ID3\x04\x00\x00", 6);
p += 6;
_AddSyncSafeInt32(id3len-10);
for (i=0; i < metadata->GetSize(); ++i)
{
const char *key;
const char *val=metadata->Enumerate(i, &key);
if (strlen(key) < 8 || strncmp(key, "ID3:", 4) || !val) continue;
key += 4;
if (!strncmp(key, "TXXX", 4))
{
memcpy(p, key, 4);
p += 4;
const char *k, *v;
int klen, vlen;
ParseUserDefMetadata(key, val, &k, &v, &klen, &vlen);
_AddSyncSafeInt32(1+klen+1+vlen);
memcpy(p, "\x00\x00\x03", 3); // UTF-8
p += 3;
memcpy(p, k, klen);
p += klen;
*p++=0;
memcpy(p, v, vlen);
p += vlen;
}
else if (!strncmp(key, "TIME", 4))
{
int tv=IsID3TimeVal(val);
if (tv)
{
memcpy(p, key, 4);
p += 4;
_AddSyncSafeInt32(1+4);
memcpy(p, "\x00\x00\x03", 3); // UTF-8
p += 3;
memcpy(p, val, 2);
if (tv == 1) memcpy(p+2, val+2, 2);
else memcpy(p+2, val+3, 2);
p += 4;
}
}
else if (key[0] == 'T' && strlen(key) == 4)
{
memcpy(p, key, 4);
p += 4;
int len=strlen(val);
_AddSyncSafeInt32(1+len);
memcpy(p, "\x00\x00\x03", 3); // UTF-8
p += 3;
memcpy(p, val, len);
p += len;
}
else if (!strcmp(key, "COMM") || !strcmp(key, "USLT"))
{
// http://www.loc.gov/standards/iso639-2/php/code_list.php
// most apps ignore this, itunes wants "eng" or something locale-specific
const char *lang=NULL;
if (!strcmp(key, "USLT")) lang=metadata->Get("ID3:LYRIC_LANG");
if (!lang) lang=metadata->Get("ID3:COMM_LANG");
if (!lang) lang=metadata->Get("ID3:COMMENT_LANG");
memcpy(p, key, 4);
p += 4;
int len=strlen(val);
_AddSyncSafeInt32(5+len);
memcpy(p, "\x00\x00\x03", 3); // UTF-8
p += 3;
if (lang && strlen(lang) >= 3 &&
tolower(*lang) >= 'a' && tolower(*lang) <= 'z')
{
*p++=tolower(*lang++);
*p++=tolower(*lang++);
*p++=tolower(*lang++);
*p++=0;
}
else
{
// some apps write "XXX" for "no particular language"
memcpy(p, "XXX\x00", 4);
p += 4;
}
memcpy(p, val, len);
p += len;
}
else if (!strncmp(key, "CHAP", 4) && chapcnt < 255)
{
const char *c1=strchr(val, ':');
const char *c2 = c1 ? strchr(c1+1, ':') : NULL;
if (c1)
{
// note, the encoding ignores the chapter number (CHAP001, etc)
++chapcnt;
const char *toc_entry=key; // use "CHAP001", etc as the internal toc entry
const char *chap_name = c2 ? c2+1 : NULL;
int st=atoi(val);
int et=atoi(c1+1);
int framelen=strlen(toc_entry)+1+16;
if (chap_name) framelen += 10+1+strlen(chap_name)+1;
memcpy(p, "CHAP", 4);
p += 4;
_AddSyncSafeInt32(framelen);
memset(p, 0, 2);
p += 2;
memcpy(p, toc_entry, strlen(toc_entry)+1);
p += strlen(toc_entry)+1;
_AddInt32(st);
_AddInt32(et);
memset(p, 0, 8);
p += 8;
if (chap_name)
{
int name_framelen=1+strlen(chap_name)+1;
memcpy(p, "TIT2", 4);
p += 4;
_AddSyncSafeInt32(name_framelen);
memcpy(p, "\x00\x00\x03", 3); // UTF-8
p += 3;
memcpy(p, chap_name, strlen(chap_name)+1);
p += strlen(chap_name)+1;
}
}
}
}
if (chapcnt)
{
int toc_framelen=strlen(CTOC_NAME)+1+2+toc.GetSize();
memcpy(p, "CTOC", 4);
p += 4;
_AddSyncSafeInt32(toc_framelen);
memset(p, 0, 2);
p += 2;
memcpy(p, CTOC_NAME, strlen(CTOC_NAME)+1);
p += strlen(CTOC_NAME)+1;
*p++=3; // CTOC flags: &1=top level, &2=ordered
*p++=(chapcnt&0xFF);
memcpy(p, toc.Get(), toc.GetSize());
p += toc.GetSize();
}
if (apic_hdr.GetSize() && apic_datalen)
{
memcpy(p, "APIC", 4);
p += 4;
int len=apic_hdr.GetSize()+apic_datalen;
_AddSyncSafeInt32(len);
memcpy(p, "\x00\x00", 2);
p += 2;
memcpy(p, apic_hdr.Get(), apic_hdr.GetSize());
p += apic_hdr.GetSize();
FILE *fp=fopenUTF8(apic_fn, "rb");
if (WDL_NORMALLY(fp))
{
fread(p, 1, apic_datalen, fp);
fclose(fp);
}
else // uh oh
{
memset(p, 0, apic_datalen);
}
p += apic_datalen;
}
if (ixml.GetSize())
{
memcpy(p, "PRIV", 4);
p += 4;
int len=ixml.GetSize()+5;
_AddSyncSafeInt32(len);
memcpy(p, "\x00\x00", 2);
p += 2;
memcpy(p, "iXML\x00", 5);
p += 5;
memcpy(p, ixml.Get(), ixml.GetSize());
p += ixml.GetSize();
}
if (xmp.GetSize())
{
memcpy(p, "PRIV", 4);
p += 4;
int len=xmp.GetSize()+4;
_AddSyncSafeInt32(len);
memcpy(p, "\x00\x00", 2);
p += 2;
memcpy(p, "XMP\x00", 4);
p += 4;
memcpy(p, xmp.Get(), xmp.GetSize());
p += xmp.GetSize();
}
if (rawtags)
{
for (int i=0; i < rawtags->GetSize(); ++i)
{
ID3RawTag *rawtag=rawtags->Get(i);
if (WDL_NORMALLY(rawtag && rawtag->key[0] && rawtag->val.GetSize()))
{
memcpy(p, rawtag->key, strlen(rawtag->key));
p += strlen(rawtag->key);
int vallen=rawtag->val.GetSize(); // includes flags
_AddSyncSafeInt32(vallen-2);
memcpy(p, rawtag->val.Get(), vallen);
p += vallen;
}
}
}
if (WDL_NOT_NORMALLY(p-buf != id3len)) hb->Resize(olen);
}
}
return hb->GetSize()-olen;
}
double ReadMetadataPrefPos(WDL_StringKeyedArray<char*> *metadata, double srate)
{
if (!metadata) return -1.0;
const char *v=metadata->Get("BWF:TimeReference");
if (!v || !v[0]) v=metadata->Get("ID3:TXXX:TIME_REFERENCE");
if (!v || !v[0]) v=metadata->Get("VORBIS:TIME_REFERENCE");
if (v && v[0] && srate > 0.0)
{
WDL_UINT64 i=ParseUInt64(v);
return (double)i/srate;
}
v=metadata->Get("IXML:BEXT:BWF_TIME_REFERENCE_LOW");
if (v && v[0] && srate > 0.0)
{
WDL_UINT64 ipos=atoi(v);
v=metadata->Get("IXML:BEXT:BWF_TIME_REFERENCE_HIGH");
if (v && v[0]) ipos |= ((WDL_UINT64)atoi(v))<<32;
return (double)ipos/srate;
}
v=metadata->Get("XMP:dm/relativeTimestamp");
if (v && v[0])
{
WDL_UINT64 i=ParseUInt64(v);
return (double)i/1000.0;
}
return -1.0;
}
// nch 0 means channel count agnostic
// nch -1 means high order ambisonic, nch must? be an integer squared, layout tag must be or'd with the number of channels
struct ChanLayout { const char *fmts; int nch; const char *desc; int chan_layout, chan_mask; };
static const ChanLayout CHAN_LAYOUTS[]=
{
// using Ls/Rs for left side/right side
// using Lb/Rb for left back/right back
{ "cw", 2, "L R",
kAudioChannelLayoutTag_UseChannelBitmap,
kAudioChannelBit_Left | kAudioChannelBit_Right },
{ "cw", 3, "L R C",
kAudioChannelLayoutTag_UseChannelBitmap,
kAudioChannelBit_Left | kAudioChannelBit_Right | kAudioChannelBit_Center },
{ "cw", 4, "L R Lb Rb",
kAudioChannelLayoutTag_UseChannelBitmap,
kAudioChannelBit_Left | kAudioChannelBit_Right |
kAudioChannelBit_LeftSurround | kAudioChannelBit_RightSurround },
{ "cw", 5, "L R C Lb Rb",
kAudioChannelLayoutTag_UseChannelBitmap,
kAudioChannelBit_Left | kAudioChannelBit_Right | kAudioChannelBit_Center |
kAudioChannelBit_LeftSurround | kAudioChannelBit_RightSurround },
{ "cw", 6, "L R C LFE Lb Rb",
kAudioChannelLayoutTag_UseChannelBitmap,
kAudioChannelBit_Left | kAudioChannelBit_Right | kAudioChannelBit_Center |
kAudioChannelBit_LFEScreen |
kAudioChannelBit_LeftSurround | kAudioChannelBit_RightSurround },
{ "cw", 6, "L R Lb Rb Ls Rs",
kAudioChannelLayoutTag_UseChannelBitmap,
kAudioChannelBit_Left | kAudioChannelBit_Right |
kAudioChannelBit_LeftSurround | kAudioChannelBit_RightSurround |
kAudioChannelBit_LeftSurroundDirect | kAudioChannelBit_RightSurroundDirect },
{ "cw", 8, "L R C LFE Lb Rb Ls Rs",
kAudioChannelLayoutTag_UseChannelBitmap,
kAudioChannelBit_Left | kAudioChannelBit_Right | kAudioChannelBit_Center |
kAudioChannelBit_LFEScreen |
kAudioChannelBit_LeftSurround | kAudioChannelBit_RightSurround |
kAudioChannelBit_LeftSurroundDirect | kAudioChannelBit_RightSurroundDirect },
{ "c", 2, "Mid-Side", kAudioChannelLayoutTag_MidSide, 0 },
{ "c", 2, "Binaural", kAudioChannelLayoutTag_Binaural, 0 },
{ "c", 4, "Ambisonic B-Format - W X Y Z", kAudioChannelLayoutTag_Ambisonic_B_Format, 0 },
{ "c", 6, "MPEG 5.1A - L R C LFE Lb Rb", kAudioChannelLayoutTag_MPEG_5_1_A, 0 },
{ "c", 6, "MPEG 5.1B - L R Lb Rb C LFE", kAudioChannelLayoutTag_MPEG_5_1_B, 0 },
{ "c", 6, "MPEG 5.1C - L C R Lb Rb LFE", kAudioChannelLayoutTag_MPEG_5_1_C, 0 },
{ "c", 6, "MPEG 5.1D - C L R Lb Rb LFE", kAudioChannelLayoutTag_MPEG_5_1_D, 0 },
{ "c", 8, "MPEG 7.1A - L R C LFE Lb Rb Lc Rc", kAudioChannelLayoutTag_MPEG_7_1_A, 0 },
{ "c", 8, "MPEG 7.1B - C Lc Rc L R Lb Rb LFE", kAudioChannelLayoutTag_MPEG_7_1_B, 0 },
{ "c", 8, "MPEG 7.1C (SMPTE 7.1) - L R C LFE Ls Rs Lb Rb", kAudioChannelLayoutTag_MPEG_7_1_C, 0 },
{ "c", 6, "ITU 3.2.1 - L R C LFE Lb Rb", kAudioChannelLayoutTag_ITU_3_2_1, 0 },
{ "c", 8, "ITU 3.4.1 - L R C LFE Lb Rb Rls Rrs", kAudioChannelLayoutTag_ITU_3_4_1, 0 },
{ "c", -1, "HO Ambisonic SN3D", kAudioChannelLayoutTag_HOA_ACN_SN3D, 0 },
{ "c", -1, "HO Ambisonic N3D", kAudioChannelLayoutTag_HOA_ACN_N3D, 0 },
{ "c", 8, "Atmos 5.1.2 - L R C LFE Lb Rb Ltm Rtm", kAudioChannelLayoutTag_Atmos_5_1_2, 0 },
{ "c", 12, "Atmos 7.1.4 - L R C LFE Lb Rb Rls Rrs Ltf Rtf Ltr Rtr", kAudioChannelLayoutTag_Atmos_7_1_4, 0 },
{ "c", 16, "Atmos 9.1.6 - L R C LFE Lb Rb Rls Rrs Lw Rw Ltf Rtf Ltm Rtm Ltr Rtr", kAudioChannelLayoutTag_Atmos_9_1_6, 0 },
};
#define LAYOUT_MASK_VALID(layout, mask) \
(((layout) == kAudioChannelLayoutTag_UseChannelBitmap) == ((mask) != 0))
const char *EnumSupportedChannelLayouts(int idx, char fmt)
{
int n=0;
for (int i=0; i < sizeof(CHAN_LAYOUTS)/sizeof(CHAN_LAYOUTS[0]); ++i)
{
WDL_ASSERT(LAYOUT_MASK_VALID(CHAN_LAYOUTS[i].chan_layout, CHAN_LAYOUTS[i].chan_mask));
if ((!fmt || strchr(CHAN_LAYOUTS[i].fmts, fmt)) && idx == n++)
{
return CHAN_LAYOUTS[i].desc;
}
}
return NULL;
}
const char *GetChannelLayoutDesc(int chan_layout, int chan_mask)
{
if (!LAYOUT_MASK_VALID(chan_layout, chan_mask)) return NULL;
bool is_match=false;
for (int i=0; i < sizeof(CHAN_LAYOUTS)/sizeof(CHAN_LAYOUTS[0]); ++i)
{
if (CHAN_LAYOUTS[i].chan_mask != 0)
{
if (CHAN_LAYOUTS[i].chan_mask == chan_mask) is_match=true;
}
else if (CHAN_LAYOUTS[i].nch == -1)
{
// high order ambisonic layout tags are OR'd with the actual number of channels
if ((CHAN_LAYOUTS[i].chan_layout&0xFFFF0000) == (chan_layout&0xFFFF0000)) is_match=true;
}
else
{
if (CHAN_LAYOUTS[i].chan_layout == chan_layout) is_match=true;
}
if (is_match)
{
return CHAN_LAYOUTS[i].desc;
}
}
return NULL;
}
bool GetChannelLayoutFromDesc(const char *desc, int *chan_layout, int *chan_mask, int *nch)
{
if (!desc || !desc[0]) return false;
for (int i=0; i < sizeof(CHAN_LAYOUTS)/sizeof(CHAN_LAYOUTS[0]); ++i)
{
if (!strcmp(desc, CHAN_LAYOUTS[i].desc))
{
if (chan_layout) *chan_layout=CHAN_LAYOUTS[i].chan_layout;
if (chan_mask) *chan_mask=CHAN_LAYOUTS[i].chan_mask;
if (nch) *nch=CHAN_LAYOUTS[i].nch;
return true;
}
}
return false;
}
bool PackFlacPicBase64(WDL_StringKeyedArray<char*> *metadata,
int img_w, int img_h, int bpp, WDL_HeapBuf *hb)
{
if (!metadata || !hb || img_w <= 0 || img_h <= 0) return false;
const char *picfn=metadata->Get("FLACPIC:APIC_FILE");
const char *pictype=metadata->Get("FLACPIC:APIC_TYPE");
const char *picdesc=metadata->Get("FLACPIC:APIC_DESC");
if (!picfn || !picfn[0]) return false;
if (!pictype) pictype="3";
if (!picdesc) picdesc="";
const char *mime=NULL;
const char *ext=WDL_get_fileext(picfn);
if (ext && (!stricmp(ext, ".jpg") || !stricmp(ext, ".jpeg"))) mime="image/jpeg";
else if (ext && !stricmp(ext, ".png")) mime="image/png";
if (!mime) return false;
WDL_FileRead fr(picfn);
if (!fr.IsOpen()) return false;
WDL_INT64 datalen64=fr.GetSize();
if (datalen64 < 1 || datalen64 >= (1<<30))
{
return false;
}
int datalen = (int)datalen64;
int r8 = (datalen&7) ? 8-(datalen&7) : 0;
// see opusfile src/info.c opus_picture_tag_parse_impl
// for what we are apparently encoding
int mimelen=strlen(mime);
int desclen=strlen(picdesc);
int binlen =
4+ // pictype
4+mimelen+
4+desclen+
4+4+4+4+ // w, h, depth, colors
4+datalen+r8;
WDL_HeapBuf hb_bin;
unsigned char *p=(unsigned char*)hb_bin.ResizeOK(binlen);
if (!p)
{
return false;
}
unsigned char *op=p;
int t=atoi(pictype);
_AddInt32(t);
_AddInt32(mimelen);
memcpy(p, mime, mimelen);
p += mimelen;
_AddInt32(desclen);
memcpy(p, picdesc, desclen);
p += desclen;
_AddInt32(img_w);
_AddInt32(img_h);
_AddInt32(bpp);
_AddInt32(0);
_AddInt32(datalen+r8);
fr.Read(p, datalen);
p += datalen;
memset(p, 0, r8);
p += r8;
if (WDL_NORMALLY(p-op == binlen))
{
int base64len=binlen*4/3;
if (base64len&3) base64len += 4-(base64len&3);
++base64len; // nul terminated return
int osz=hb->GetSize();
char *pout=(char*)hb->ResizeOK(osz+base64len);
if (pout)
{
wdl_base64encode(op, pout, binlen);
return true;
}
}
return false;
}
bool ExportMetadataImageToTmpFile(const char *srcfn, const char *infostr,
WDL_FastString *imgdesc, WDL_FastString *imgtype, WDL_FastString *imgfn)
{
if (!srcfn || !srcfn[0] || !infostr || !infostr[0] || !imgfn) return false;
WDL_HeapBuf tmp;
int tmplen=strlen(infostr);
if (!tmp.ResizeOK(tmplen+1)) return false;
memcpy(tmp.Get(), infostr, tmplen+1);
const char *ext=NULL, *mime=NULL, *desc=NULL, *type=NULL, *offs=NULL, *len=NULL;
char *p=(char*)tmp.Get();
for (int i=0; i < tmplen; ++i)
{
if (!strncmp(p+i, "ext:", 4)) { if (i) p[i-1]=0; i += 4; ext=p+i; }
else if (!strncmp(p+i, "mime:", 5)) { if (i) p[i-1]=0; i += 5; mime=p+i; }
else if (!strncmp(p+i, "desc:", 5)) { if (i) p[i-1]=0; i += 5; desc=p+i; }
else if (!strncmp(p+i, "type:", 5)) { if (i) p[i-1]=0; i += 5; type=p+i; }
else if (!strncmp(p+i, "offset:", 7)) { if (i) p[i-1]=0; i += 7; offs=p+i; }
else if (!strncmp(p+i, "length:", 7)) { if (i) p[i-1]=0; i += 7; len=p+i; }
}
WDL_INT64 ioffs = offs ? (WDL_INT64)atof(offs) : 0;
int ilen = len ? atoi(len) : 0;
bool ok=false;
if ((ext || mime) && ioffs > 0 && ilen > 0)
{
WDL_FileRead fr(srcfn);
if (fr.IsOpen() && fr.GetSize() >= ioffs+ilen)
{
fr.SetPosition(ioffs);
char tmppath[2048];
tmppath[0]=0;
GetTempPath(sizeof(tmppath), tmppath);
imgfn->Set(tmppath);
imgfn->Append(WDL_get_filepart(srcfn));
imgfn->Append(".");
if (ext) imgfn->Append(ext);
else if (mime && !strncmp(mime, "image/", 6)) imgfn->Append(mime+6);
WDL_FileWrite fw(imgfn->Get());
if (fw.IsOpen() && CopyFileData(&fr, &fw, ilen))
{
if (desc && imgdesc) imgdesc->Set(desc);
if (type && imgtype) imgtype->Set(type);
ok=true;
}
}
}
return ok;
}
#endif // _METADATA_H_