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

Reply via email to