#ifndef _WDL_METADATA_H_ #define _WDL_METADATA_H_ #include #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 *metadata, WDL_TypedBuf *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 *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 *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("<"); break; case '>': str->Append(">"); break; case '&': str->Append("&"); break; case ' ': str->Append(is_value ? " " : "_"); break; default: str->Append(&c,1); break; } } } const char *XMLHasOpenTag(WDL_FastString *str, const char *tag) // tag like "") { // 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 *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 *metadata) { if (!buf || !buflen || !metadata) return false; while (buflen > 20 && strnicmp(buf, "", 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 *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 *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 *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 *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 *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 *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=""; 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, "", 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", key, hi, key); ixml.AppendFormatted(4096, "<%s_LOW>%d", 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(""); } if (need_close) { ixml.AppendFormatted(512, "", need_close); } if (ixml.GetLength()) { ixml.Append(""); 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 *metadata) { if (!hb || !metadata) return 0; if (!HasScheme("XMP", metadata)) return 0; int olen=hb->GetSize(); static const char *xmp_hdr= "" "" "" "" "" ""; 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, "", lang); xmp.Append(val); xmp.Append(""); xmp.AppendFormatted(1024, "", 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= "" "" "" "Cue" "f"; static const char *track_hdr2= "" "" ""; static const char *track_ftr= "" "" "" "" ""; 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(""); xmp.AppendFormatted(1024, "%.0f", st); if (et > st) xmp.AppendFormatted(1024, "%.0f", et-st); if (name[0]) xmp.AppendFormatted(1024, "%s", name); xmp.Append(""); } 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 *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 *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 *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 *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 *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 *mex_metadata, WDL_StringKeyedArray *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 *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 *src, WDL_StringKeyedArray *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 *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 *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 *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 *metadata, bool want_embed_otherschemes, int *ixml_lenwritten, int ixml_padtolen, WDL_PtrList *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 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 *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 *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_