Hey,
I no longer use Mixxx or other software-based mixing (at least for the moment)
but figured someone here may be interested to know the "AudioID" binary blob in
a Traktor DJ playlist's entry is actually a base64-encoded visual thumbnail of
the track's WAV, squeezed to 512-pixels wide, with each 4-bit nybble
representing a volume. The image is mirrored vertically. I guess it's used as a
preview while tracks are deep-scanned.
I attached my source here, the thumbnail decoder function is at the end,
nmlEntry::SaveWAVThumbnail(), the rest is rubbish.
cheers,
-- p
// Native Instruments' Traktor DJ NML playlist decoder
// #include <regex> // not supported until gcc 4.9 ?
// #include <tuple>
/*
- RELEASE_DATE is optional
*/
#include "wx/filename.h"
#include "wx/base64.h"
#include "wx/wfstream.h"
#include "wx/txtstrm.h"
#include "wx/sstream.h"
#include "wx/regex.h"
#include "Controller.h"
#include "TraktorDec.h"
using namespace LX;
using namespace std;
enum FIELD_TYPES : int
{
FIELD_ILLEGAL = 0,
FIELD_STRING,
FIELD_INTEGER,
FIELD_DOUBLE, // front space-padding is ok
FIELD_UNIX_PATH,
FIELD_FILE_NAME,
FIELD_OS_PATH,
FIELD_OS_VOLUME,
FIELD_DATE, // YYYY/MM/DD
FIELD_MARKER_NAME, // { Cue | Beat Marker | Fade In | Fade Out } or { first moan | aha! | beat start | beat restart | restart | buildup start }
FIELD_ENC64,
FIELD_STRING_LIST // for CUEs
};
enum TAG_TYPES : int
{
TTYP_ARTIST = 0,
TTYP_AUDIO_ID,
TTYP_ID,
TTYP_LOCK,
TTYP_TRACK_TITLE,
TTYP_DIR,
TTYP_FILE,
TTYP_PATH,
TTYP_DISK_VOLUME,
TTYP_ALBUM,
TTYP_TRACK_NUMBER,
TTYP_BITRATE,
TTYP_COMMENT,
TTYP_GENRE,
TTYP_IMPORT_DATE,
TTYP_KEY_LYRICS,
TTYP_LAST_PLAYED,
TTYP_PLAYCOUNT,
TTYP_PLAYTIME,
TTYP_RANKING,
TTYP_RELEASE_DATE,
TTYP_BPM,
TTYP_BPM_QUALITY,
TTYP_PEAK_DB,
TTYP_PERCEIVED_DB,
TTYP_LABEL,
TTYP_POS,
TTYP_TYPE
};
static
void ConvVal(const wxString &s, wxString *val_s)
{
wxASSERT(val_s);
*val_s = s;
}
static
void ConvVal(const wxString &s, int *ip)
{
wxASSERT(ip);
long l = 0;
bool ok = s.ToLong(&l);
wxASSERT(ok);
*ip = (int) l;
}
static
void ConvVal(const wxString &s, double *dp)
{
wxASSERT(dp);
bool ok = s.ToDouble(dp);
wxASSERT(ok);
}
static
void ConvVal(const wxString &s, wxDateTime *dt)
{
wxASSERT(dt);
bool ok = dt->ParseDate(s);
wxASSERT(ok);
}
//---- CTOR -------------------------------------------------------------------
TraktorDecoder::TraktorDecoder(Controller *controller)
{
wxASSERT(controller);
m_Controller = controller;
m_PlaylistEntries.clear();
m_TagToInfoMap.clear();
#if 0
m_TypeToConvMap =
{
{FIELD_STRING, {&ValToStr}},
{FIELD_INTEGER, {&ValToInt}},
{FIELD_DOUBLE, {&ValToDouble}}, // may be space-padded in front
{FIELD_UNIX_PATH, {&ValToStr}},
{FIELD_FILE_NAME, {&ValToStr}},
{FIELD_OS_PATH, {&ValToStr}},
{FIELD_OS_VOLUME, {&ValToStr}},
{FIELD_DATE, {&ValToDate}}, // YYYY/MM/DD
{FIELD_MARKER_NAME, {&ValToStr}}, // { Cue | Beat Marker | Fade In | Fade Out } or { first moan | aha! | beat start | beat restart | restart | buildup start }
{FIELD_ENC64, {&ValToStr}},
{FIELD_STRING_LIST, {&ValToStr}}, // for CUEs
};
#endif
// use ___(unique)___ (R)aw string delimiter to use double-quotes within, even if preceding a ')'
bool ok = m_EntryRE.Compile(R"___(.*?<ENTRY(.*?)</ENTRY>)___", wxRE_ADVANCED);
wxASSERT(ok && m_EntryRE.IsValid());
ok = m_FieldRE.Compile(R"___(.*? ([A-Z_]+)="(.*?)")___", wxRE_ADVANCED);
wxASSERT(ok && m_FieldRE.IsValid());
// few duplicate tags despite XML hierarchy, except for
// labels (list)
// title (duplicate)
m_TagToInfoMap =
{
{"ARTIST", {TTYP_ARTIST, FIELD_STRING}},
{"AUDIO_ID", {TTYP_AUDIO_ID, FIELD_ENC64}},
{"ID", {TTYP_ID, FIELD_INTEGER}},
{"LOCK", {TTYP_LOCK, FIELD_INTEGER}},
{"TITLE", {TTYP_TRACK_TITLE, FIELD_STRING}},
{"DIR", {TTYP_DIR, FIELD_UNIX_PATH}},
{"FILE", {TTYP_FILE, FIELD_FILE_NAME}},
{"PATH", {TTYP_PATH, FIELD_OS_PATH}},
{"VOLUME", {TTYP_DISK_VOLUME, FIELD_OS_VOLUME}},
{"ALBUM", {TTYP_ALBUM, FIELD_STRING}},
{"TRACK", {TTYP_TRACK_NUMBER, FIELD_INTEGER}},
{"BITRATE", {TTYP_BITRATE, FIELD_INTEGER}},
{"COMMENT", {TTYP_COMMENT, FIELD_STRING}},
{"GENRE", {TTYP_GENRE, FIELD_STRING}},
{"IMPORT_DATE", {TTYP_IMPORT_DATE, FIELD_DATE}},
{"KEY_LYRICS", {TTYP_KEY_LYRICS, FIELD_STRING}},
{"LAST_PLAYED", {TTYP_LAST_PLAYED, FIELD_DATE}},
{"PLAYCOUNT", {TTYP_PLAYCOUNT, FIELD_INTEGER}},
{"PLAYTIME", {TTYP_PLAYTIME, FIELD_INTEGER}},
{"RANKING", {TTYP_RANKING, FIELD_INTEGER}},
{"RELEASE_DATE", {TTYP_RELEASE_DATE, FIELD_DATE}},
{"BPM", {TTYP_BPM, FIELD_DOUBLE}},
{"BPM_QUALITY", {TTYP_BPM_QUALITY, FIELD_INTEGER}},
{"PEAK_DB", {TTYP_PEAK_DB, FIELD_DOUBLE}},
{"PERCEIVED_DB", {TTYP_PERCEIVED_DB, FIELD_DOUBLE}}, // may be negative
// list
{"LABEL", {TTYP_LABEL, FIELD_STRING_LIST}}, // is LIST (not limited string set, despite appearances)
{"POS", {TTYP_POS, FIELD_DOUBLE}},
{"TYPE", {TTYP_TYPE, FIELD_INTEGER}} // actually limited integer set
};
}
//---- DTOR -------------------------------------------------------------------
TraktorDecoder::~TraktorDecoder()
{
m_Controller = nil;
}
//---- Reset Decoder ----------------------------------------------------------
void TraktorDecoder::Reset(void)
{
m_PlaylistEntries.clear();
m_FileNameToIndexMap.clear();
m_TitleToIndexMap.clear();
m_ArtistTitleToIndexMap.clear();
}
//---- Index Entries ----------------------------------------------------------
void TraktorDecoder::IndexEntries(void)
{
m_FileNameToIndexMap.clear();
m_TitleToIndexMap.clear();
m_ArtistTitleToIndexMap.clear();
for (int i = 0; i < m_PlaylistEntries.size(); i++)
{
const nmlEntry &ne = m_PlaylistEntries[i];
m_FileNameToIndexMap[ne.GetShortFileName()] = i;
m_TitleToIndexMap[ne.GetTrackTitle()] = i;
const wxString artist_title = ne.GetArtist() + ne.GetTrackTitle();
m_ArtistTitleToIndexMap[artist_title] = i;
}
}
//---- Has Indexed Entries ? --------------------------------------------------
bool TraktorDecoder::HasIndexedEntries(void) const
{
bool f = (m_FileNameToIndexMap.size() > 0) && (m_TitleToIndexMap.size() > 0);
return f;
}
//---- Get Short Filename Index -----------------------------------------------
int TraktorDecoder::GetFileNameIndex(const wxString &short_fn)
{
if (m_FileNameToIndexMap.count(short_fn) == 0)
return -1; // not found
else return m_FileNameToIndexMap[short_fn];
}
//---- Get Track Title Index --------------------------------------------------
int TraktorDecoder::GetTrackTitleIndex(const wxString &track_title)
{
if (m_TitleToIndexMap.count(track_title) == 0)
return -1; // not found
else return m_TitleToIndexMap[track_title];
}
//---- Get Artist + Title Index -----------------------------------------------
int TraktorDecoder::GetArtistTitleIndex(const wxString &artist_n_title)
{
if (m_ArtistTitleToIndexMap.count(artist_n_title) == 0)
return -1; // not found
else return m_ArtistTitleToIndexMap[artist_n_title];
}
//---- Playlist Entry defaults ------------------------------------------------
void nmlEntry::Defaults(void)
{
m_Artist = m_TrackTitle = m_Album = m_Comment = m_Genres = m_KeyLyrics = wxEmptyString;
m_Location.m_Dir = m_Location.m_Path = m_Location.m_File = m_Location.m_DiskVolume = wxEmptyString;
m_ID = m_Lock = m_TrackNumber = m_BitRate = m_PlayCount = m_PlayTime = m_Ranking = m_BPMPrecision = 0;
m_PeakDB = m_PerceivedDB = m_BPM = -1;
m_CuePoints.clear();
m_Index = 0;
}
//---- Dump Entry -------------------------------------------------------------
void nmlEntry::Dump(void) const
{
wxString s;
s.Printf("\n\n# %d\n", m_Index);
s << wxString::Format("Artist \"%s\"\n", m_Artist);
s << wxString::Format("Title \"%s\"\n", m_TrackTitle);
s << wxString::Format("Album \"%s\"\n", m_Album);
s << wxString::Format("Genre \"%s\"\n", m_Genres);
s << wxString::Format("BPM %f\n\n", m_BPM);
wxLogMessage(s);
}
//---- Get Index --------------------------------------------------------------
int nmlEntry::GetIndex(void) const
{
return m_Index;
}
//---- Get Artist -------------------------------------------------------------
wxString nmlEntry::GetArtist(void) const
{
return m_Artist;
}
//---- Get Track Title --------------------------------------------------------
wxString nmlEntry::GetTrackTitle(void) const
{
return m_TrackTitle;
}
//---- Get Short Filename -----------------------------------------------------
wxString nmlEntry::GetShortFileName(void) const
{
return m_Location.m_File;
}
//---- Dump Playlist ----------------------------------------------------------
void TraktorDecoder::Dump(void)
{
for (const auto &it : m_PlaylistEntries)
{
const nmlEntry &ne = it;
ne.Dump();
}
}
//---- Decode Field -----------------------------------------------------------
void TraktorDecoder::DecodeField(const wxString &k_s, const wxString &v, nmlEntry &ne)
{
wxASSERT_MSG(m_TagToInfoMap.count(k_s) == 1, wxString::Format("Traktor key \"%s\" not found", k_s));
const TInfo &tinfo = m_TagToInfoMap[k_s];
const int k = tinfo.m_KeyType;
/*
const int typ = tinfo.m_ValueType;
wxASSERT(m_TypeToConvMap.count(typ) == 1);
ConvInfo cinfo = m_TypeToConvMap[typ];
convFn fn = cinfo.m_ConvFn;
*/
switch (k)
{ case TTYP_ARTIST:
ConvVal(v, &ne.m_Artist);
break;
case TTYP_AUDIO_ID:
ConvVal(v, &ne.m_AudioID);
break;
case TTYP_ID:
ConvVal(v, &ne.m_ID);
break;
case TTYP_LOCK:
ConvVal(v, &ne.m_Lock);
break;
case TTYP_TRACK_TITLE:
ConvVal(v, &ne.m_TrackTitle);
break;
case TTYP_DIR:
ConvVal(v, &ne.m_Location.m_Dir);
break;
case TTYP_FILE:
ConvVal(v, &ne.m_Location.m_File);
break;
case TTYP_PATH:
ConvVal(v, &ne.m_Location.m_Path);
break;
case TTYP_DISK_VOLUME:
ConvVal(v, &ne.m_Location.m_DiskVolume);
break;
case TTYP_ALBUM:
ConvVal(v, &ne.m_Album);
break;
case TTYP_TRACK_NUMBER:
ConvVal(v, &ne.m_TrackNumber);
break;
case TTYP_BITRATE:
ConvVal(v, &ne.m_BitRate);
break;
case TTYP_COMMENT:
ConvVal(v, &ne.m_Comment);
break;
case TTYP_GENRE:
ConvVal(v, &ne.m_Genres);
break;
case TTYP_IMPORT_DATE:
ConvVal(v, &ne.m_ImportDate);
break;
case TTYP_KEY_LYRICS:
ConvVal(v, &ne.m_KeyLyrics);
break;
case TTYP_LAST_PLAYED:
ConvVal(v, &ne.m_LastPlayedDate);
break;
case TTYP_PLAYCOUNT:
ConvVal(v, &ne.m_PlayCount);
break;
case TTYP_PLAYTIME:
ConvVal(v, &ne.m_PlayTime);
break;
case TTYP_RANKING:
ConvVal(v, &ne.m_Ranking);
break;
case TTYP_RELEASE_DATE:
ConvVal(v, &ne.m_ReleaseDate);
break;
case TTYP_BPM:
ConvVal(v, &ne.m_BPM);
break;
case TTYP_BPM_QUALITY:
ConvVal(v, &ne.m_BPMPrecision);
break;
case TTYP_PEAK_DB:
ConvVal(v, &ne.m_PeakDB);
break;
case TTYP_PERCEIVED_DB:
ConvVal(v, &ne.m_PerceivedDB);
break;
// LIST
case TTYP_LABEL:
// (push, then fill)
ne.m_CuePoints.push_back(CuePoint());
ConvVal(v, &ne.m_CuePoints.back().m_Name);
break;
case TTYP_POS:
ConvVal(v, &ne.m_CuePoints.back().m_PosMS);
break;
case TTYP_TYPE:
ConvVal(v, &ne.m_CuePoints.back().m_Type);
break;
default:
wxFAIL_MSG("unrecognized Traktor tag");
break;
}
}
//---- Process Entry ----------------------------------------------------------
void TraktorDecoder::ProcessEntry(wxString entry)
{
wxASSERT(!entry.IsEmpty());
// de-duplicates into uniques (this prevents duplicate "title")
entry.Replace("ALBUM TITLE", "TITLE ALBUM");
nmlEntry ne;
const wxChar *field_p = entry.c_str();
while (m_FieldRE.Matches(field_p))
{ size_t field_match_start = 0, field_match_len = 0;
bool f = m_FieldRE.GetMatch(&field_match_start, &field_match_len, 0/*whole field match*/);
wxASSERT(f);
size_t ki = 0, kl = 0;
f = m_FieldRE.GetMatch(&ki, &kl, 1);
wxASSERT(f);
const wxString k_s(field_p + ki, kl);
size_t vi = 0, vl = 0;
f = m_FieldRE.GetMatch(&vi, &vl, 2);
wxASSERT(f);
const wxString val(field_p + vi, vl);
DecodeField(k_s, val, ne/*&*/);
field_p += field_match_len;
}
// (internal index)
ne.m_Index = m_PlaylistEntries.size();
m_PlaylistEntries.push_back(ne);
}
//---- Decode File ------------------------------------------------------------
size_t TraktorDecoder::DecodeFile(const wxFileName &cfn)
{
const wxString fpath = cfn.GetFullPath();
wxLogMessage("TraktorDecoder::DecodeFile(\"%s\")", fpath);
wxASSERT(cfn.IsOk() && cfn.FileExists());
const size_t sz = cfn.GetSize().GetLo();
Reset();
wxFileInputStream fis(fpath);
wxASSERT(fis.IsOk());
// load whole file ahead
wxString s_buff;
wxStringOutputStream sos(&s_buff, wxConvUTF8);
const size_t n_bytes_read = fis.Read(sos/*&*/).LastRead();
wxASSERT(n_bytes_read == sz);
const wxChar *str_p = s_buff.c_str();
int index = 0;
while (m_EntryRE.Matches(str_p))
{
size_t whole_match_start = 0, whole_match_len = 0;
bool f = m_EntryRE.GetMatch(&whole_match_start, &whole_match_len, 0/*whole match*/);
wxASSERT(f);
size_t match_start = 0, match_len = 0;
f = m_EntryRE.GetMatch(&match_start, &match_len, 1/*first match*/);
wxASSERT(f);
ProcessEntry(wxString(str_p + match_start, match_len));
str_p += whole_match_len;
index++;
}
return index;
}
//---- Get # Playlist Entries -------------------------------------------------
size_t TraktorDecoder::GetNumEntries(void) const
{
return m_PlaylistEntries.size();
}
//---- Get Nth Entry ----------------------------------------------------------
const nmlEntry& TraktorDecoder::GetNthEntry(const int &index) const
{
wxASSERT((index >= 0) && (index < m_PlaylistEntries.size()));
return m_PlaylistEntries[index];
}
//---- Get WAV Thumbnail ------------------------------------------------------
bool nmlEntry::SaveWAVThumbnail(const wxString &fname, const bool &bin_safe_f) const
{
wxASSERT(!fname.IsEmpty());
wxASSERT(!m_AudioID.IsEmpty());
wxString enc64 = m_AudioID;
const size_t unpadded_sz = enc64.Len();
wxASSERT_MSG(unpadded_sz == 342, "illegal base64 encoded size");
// pad base64 with '='
const size_t n_pad = (4 - (unpadded_sz % 4)) % 4;
enc64 += wxString('=', n_pad);
const size_t enc_b64_sz = enc64.length();
wxASSERT_MSG(enc_b64_sz == 344, "illegal base64 encoded size");
/* const size_t dec_b64_sz = wxBase64DecodedSize(enc_b64_sz); // should be 256 bytes, but wx rounds up to 258 with pad!
wxASSERT_MSG(dec_b64_sz == 256, "illegal base64 decoded size");
*/
const char *c_ascii = enc64.fn_str();
wxASSERT(c_ascii);
uint8_t buff[256];
size_t err_index = -1;
// decode64
const int32_t dec_sz = wxBase64Decode(&buff[0], sizeof(buff), c_ascii, enc_b64_sz, wxBase64DecodeMode_Strict, &err_index);
wxASSERT((dec_sz == 256) && (-1 == err_index));
if ((dec_sz != 256) || (-1 != err_index)) return false;
if (bin_safe_f)
{ // write as raw binary
wxFileOutputStream fos(fname + ".bin");
wxASSERT(fos.IsOk());
fos.WriteAll(&buff[0], dec_sz);
}
// convert bytes to nybbles
vector<uint8_t> nybbles;
const size_t UNKNOWN_HEADER_BYTES = 4; // (skip unknowns)
for (int i = UNKNOWN_HEADER_BYTES; i < 256; i++)
{
const uint8_t b = buff[i];
nybbles.push_back(b >> 4);
nybbles.push_back(b & 0xF);
}
const int h = 32;
wxBitmap bm(nybbles.size(), h, 32/*depth*/);
wxMemoryDC dc(bm);
// erase bitmap
dc.SetBrush(wxBrush(wxColour(0, 0, 0, 0), wxSOLID));
dc.SetPen(*wxTRANSPARENT_PEN);
dc.DrawRectangle(0, 0, bm.GetWidth(), h);
// draw WAV thumbnail
dc.SetDeviceOrigin(0, h / 2);
const wxColour LX_ORANGE_COLOR = wxColor(255, 180, 20, 255);
const wxPen orangePen(LX_ORANGE_COLOR, 1, wxSOLID);
dc.SetPen(orangePen);
for (int i = 0; i < nybbles.size(); i++)
{
int dy = nybbles[i];
dc.DrawLine(i, -dy, i, dy);
}
dc.SelectObject(wxNullBitmap);
// save thumbnail bitmap
bool ok = bm.SaveFile(fname + ".png", wxBITMAP_TYPE_PNG);
wxASSERT(ok);
return ok;
}
// nada mas
------------------------------------------------------------------------------
Rapidly troubleshoot problems before they affect your business. Most IT
organizations don't have a clear picture of how application performance
affects their revenue. With AppDynamics, you get 100% visibility into your
Java,.NET, & PHP application. Start your 15-day FREE TRIAL of AppDynamics Pro!
http://pubads.g.doubleclick.net/gampad/clk?id=84349831&iu=/4140/ostg.clktrk
_______________________________________________
Get Mixxx, the #1 Free MP3 DJ Mixing software Today
http://mixxx.org
Mixxx-devel mailing list
Mixxx-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/mixxx-devel