// Copyright (C) Mikko Apo (apo@iki.fi)
// The following code may be used to write free software
// if credit is given to the original author.
// Using it for anything else is not allowed without permission
// from the author.

/*
Revision history:
  2.01	Mono signal analysis now actually shows the real dc offset.
  2.0	First release of stereo/mono-in version.
  1.0	First release. Mono in only.
  */


#include <stdlib.h>
#include <time.h>
#include <math.h>
#include <string.h>
#include "../mdk.h"

#define miCOMMAND_STRING "Show analysis...\nReset analysis\nAbout..."
#define miMACHINE_NAME "cheapo dc"
#define miSHORT_NAME "ch.dc"
#define miMACHINE_AUTHOR "Mikko Apo (apo@iki.fi)"
#define miMAX_TRACKS		0
#define miMIN_TRACKS		0
#define miNUMGLOBALPARAMETERS 5
#define miNUMTRACKPARAMETERS 0
#define miNUMATTRIBUTES 0
#define miVERSION "2.01"

//	Parameters

CMachineParameter const paraMode = 
{ pt_byte, "Mode","Mode: 0 Statistics & Separate controlling, 1 Stats & Left master right slave, 2 Separate, 3 Right slave",0,3,0xff,MPF_STATE,0 };

CMachineParameter const paraLDC = 
{ pt_word, "Left Offset","Left Offset",0,0xfffe,0xffff,MPF_STATE,0xfffe/2 };

CMachineParameter const paraRDC = 
{ pt_word, "Right Offset","Right Offset",0,0xfffe,0xffff,MPF_STATE,0xfffe/2 };

CMachineParameter const paraInertia = 
{ pt_word, "Inertia","Inertia length",0,0xfffe,0xffff,MPF_STATE,1 };

CMachineParameter const paraInertiaUnit = 
{ pt_byte, "Inertia Unit","Inertia Unit: 0=tick (default), 1 ticks/256, 2 samples, 3=ms, 4=seconds",0,4,0xff,MPF_STATE,0 };

// List of all parameters, track parameters last

CMachineParameter const *pParameters[] = 
{ &paraMode,&paraLDC,&paraRDC,&paraInertia,&paraInertiaUnit };

#pragma pack(1)

class gvals
{
public:
	byte mode;
	word ldc;
	word rdc;
	word inertia;
	byte inertiaunit;
};

#pragma pack()

// Machine's info

CMachineInfo const MacInfo = 
{
	MT_EFFECT,MI_VERSION,MIF_DOES_INPUT_MIXING,miMIN_TRACKS,miMAX_TRACKS,
	miNUMGLOBALPARAMETERS,miNUMTRACKPARAMETERS,pParameters,miNUMATTRIBUTES,NULL,
#ifdef _DEBUG
	miMACHINE_NAME" [DEBUG]"
#else
	miMACHINE_NAME
#endif
	,miSHORT_NAME,miMACHINE_AUTHOR,miCOMMAND_STRING
};


class miex : public CMDKMachineInterfaceEx
{

};

class mi : public CMDKMachineInterface
{
public:
	mi();

	virtual void Command(int const i);
	virtual void Tick();
	virtual char const *DescribeValue(int const param, int const value);

	virtual void MDKInit(CMachineDataInput * const pi);
	virtual bool MDKWork(float *psamples, int numsamples, int const mode);
	virtual bool MDKWorkStereo(float *psamples, int numsamples, int const mode);
	virtual void MDKSave(CMachineDataOutput * const po) { }

	public:
	virtual CMDKMachineInterfaceEx *GetEx() { return &ex; }
	virtual void OutputModeChanged(bool stereo);

	public:
	miex ex;
	gvals gval;

private:

	void print_amp(char *txt, float num);
	void print_dc(char *txt, double dcsum,unsigned int dc_num);
	void print_suggest(char *txt, float max,float min);
    void print_time(char *txt,unsigned long samples);
    void print_channel(char *txt,float maxlevelfound, float minlevelfound, double dcsum);


	unsigned long calculate_length(byte type, word len);

	float l_dc,r_dc;
	double left_dcsum_in,left_dcsum_out,right_dcsum_in,right_dcsum_out;
	int valInertia,valInertiaUnit;
	bool l_inertia,r_inertia,first;	// inertia
	float l_dc_inc,l_dc_target; //
	float r_dc_inc,r_dc_target;  //
	unsigned int l_counter,r_counter; //
	unsigned long dc_counter;
	int valMode;
	bool stereo_mode;
	float left_maxlevelfound,left_minlevelfound;
	float right_maxlevelfound,right_minlevelfound;
};


DLL_EXPORTS

mi::mi()
{
	GlobalVals = &gval;
}

// Produces output for analysis

void mi::print_amp(char *txt, float num)
{
	sprintf(txt,"%ssample:%+.1f (",txt,num);
	if(num)
	{
	  sprintf(txt,"%s%.1fdB; %+.2f%%)",txt,(float)(20.0*log10(fabs(num)/( (num>0)?32767.0:32768.0) )),(float)((100*num)/( (num>0)?32767.0:32768.0)));
	} else
	{
	  sprintf(txt,"%s-inf dB; 0.00%%)",txt);
	}

	if(num>32767.0||num<-32768.0)
	{
		sprintf(txt,"%s *** Possible clipping ***",txt);
	}
	sprintf(txt,"%s\n",txt);
}

void mi::print_suggest(char *txt, float max,float min)
{
	if(max>32767.0||min<-32768.0)
	{
	    max=(float)fabs(max);
	    min=(float)fabs(min);
		sprintf(txt,"%sThe analysed signal exceeds the normal range between -32768.0 and 32767.0\nIt might clip in the following effect and it will clip if connected to master output.\n",txt);
		sprintf(txt,"%sThe input volume slider of the next machine should be set to under %d.\n",txt,(int)(16384*( (max > min) ? (32767.0/max) : (32768.0/min) ) ) );
		sprintf(txt,"%sThe master output volume slider should have a value over ",txt);
		sprintf(txt,"%s%d to prevent clipping.\n",txt,(int)(log10((max>min)?(32767.0/max):(32768.0/min))*((20.0*16384.0)/(-80.0))));
	} else
	{
		sprintf(txt,"%sThe analysed signal was in the normal range between -32768.0 and 32767.0. \nThere is no need to adjust the volume sliders.\n",txt);
	}
}

void mi::print_time(char *txt,unsigned long samples)
{
  unsigned long secs;
  unsigned long samplespersec=pMasterInfo->SamplesPerSec;
  unsigned long samplespertick=pMasterInfo->SamplesPerTick;
  secs=(unsigned int)(samples/samplespersec);
  sprintf(txt,"%s %ld ticks [time: %d:%02d:%02d.%03d (%ld samples)]\n",txt,samples/samplespertick,secs/3600,(secs%3600)/60,secs%60,(1000*(samples%(samplespersec)))/samplespersec,samples);
}

void mi::print_dc(char *txt, double dcsum,unsigned int dc_num)
{
	sprintf(txt,"%sDC Offset (average): %.1f (",txt,dcsum/dc_num);
	if(dcsum/dc_num)
	{
	  sprintf(txt,"%s%.1fdB; %+.2f%%)\n",txt,(float)(20.0*log10(fabs(dcsum/(dc_num*32768.0)))),(float)((100*(dcsum/dc_num))/32768.0));
	} else
	{
	  sprintf(txt,"%s-inf dB; 0.00%%)\n",txt);
	}
}


void mi::print_channel(char *txt,float maxlevelfound, float minlevelfound, double dcsum)
{
	float dc,offset;
	bool suggest=false;
	sprintf(txt,"%sMax ",txt);
	print_amp(txt,maxlevelfound);

	sprintf(txt,"%sMin ",txt);
	print_amp(txt,minlevelfound);

	print_dc(txt,dcsum,dc_counter);

	// calculate the correct suggestion
	dc=-(float) (dcsum/dc_counter);
	offset=0;
	sprintf(txt,"%sDC Offset Suggestion: ",txt);
	if(dc)
	{
	if(dc>0)
	{
		// check if wave fits in the graph with full dc
		if(maxlevelfound+dc<=32767)
		{
			offset=dc;
			suggest=true;
		} else
		{
			// we can't correct with full dc, so find the max correction
			if(32767>maxlevelfound)
			{
				offset=32767-maxlevelfound;
				suggest=true;
			}
		}
	}
	if(dc<0)
	{
		if(minlevelfound+dc>=-32768)
		{
			offset=dc;
			suggest=true;
		} else
		{
			if(-32768<minlevelfound)
			{
				offset=-32768-minlevelfound;
				suggest=true;
			}
		}
	}
	if(suggest)
	{
	  if(offset==dc)
	  {
	    sprintf(txt,"%sset increment to %+d [%d]\n",txt,(int)offset,(int)offset+32767);
	  } else
	  {
	    sprintf(txt,"%swithout clipping the signal, you can set the dc offset to %+d [%d]\n",txt,(int)offset,(int)offset+32767);
	  }
	} else
	{
		sprintf(txt,"%scannot fix dc offset because the signal is already clipping.\n",txt);
	}
	} else
	{
		sprintf(txt,"%sno suggestion.\n",txt);
	}
}

void mi::Command(int const i)
{
	static char txt[2000];
	switch(i)
	{
	case 0:
		if(dc_counter&&valMode<=1)
		{
		  sprintf(txt,"Sound analysis:\n\nStatistical data collected:");
		  print_time(txt,dc_counter);
		  if(stereo_mode)
		  {
			  sprintf(txt,"%s\nLeft channel:\n\n",txt);
			  print_channel(txt,left_maxlevelfound,left_minlevelfound, left_dcsum_in);
			  sprintf(txt,"%s\nRight channel:\n\n",txt);
			  print_channel(txt,right_maxlevelfound,right_minlevelfound, right_dcsum_in);
			  sprintf(txt,"%s\nDC offsets of the signals after this machine:\n\nLeft ",txt);
			  print_dc(txt,left_dcsum_out,dc_counter);
			  sprintf(txt,"%sRight ",txt);
			  print_dc(txt,right_dcsum_out,dc_counter);
			  sprintf(txt,"%s\nVolume slider suggestion based on max and min levels of both channels:\n\n",txt);
			  print_suggest(txt,(left_maxlevelfound>right_maxlevelfound)?left_maxlevelfound:right_maxlevelfound,(left_minlevelfound<right_minlevelfound)?left_minlevelfound:right_minlevelfound);
		  } else
		  {
			  sprintf(txt,"%s\nMono signal:\n\n",txt);
			  print_channel(txt,left_maxlevelfound,left_minlevelfound, left_dcsum_in);
			  sprintf(txt,"%s\nDC offset of the signal after this machine:\n\nSignal ",txt);
			  print_dc(txt,left_dcsum_out,dc_counter);
			  sprintf(txt,"%s\nVolume slider suggestion based on max and min levels:\n\n",txt);
			  print_suggest(txt,left_maxlevelfound,left_minlevelfound);
		  }
		} else
		{
			sprintf(txt,"*** No sound analysed ***\n");
		}
		pCB->MessageBox(txt);
		break;
	case 1:
		left_maxlevelfound=right_maxlevelfound=-9999999.0;
		left_minlevelfound=right_minlevelfound=9999999.0;
		left_dcsum_in=left_dcsum_out=right_dcsum_in=right_dcsum_out=0.0;
		dc_counter=0;
		break;
	case 2:
		pCB->MessageBox(miMACHINE_NAME"\n\nBuild date: "__DATE__"\nVersion: "miVERSION"\nCoded by: "miMACHINE_AUTHOR"\nThanks to #buzzdev for support.\n\nCheck out http://www.iki.fi/apo/buzz/\nfor more buzz stuff.\n\nExcellent skin made by Hymax.");
		break;
	}
}

char const *mi::DescribeValue(int const param, int const value)
{
	static char txt[100];

	switch(param)
	{
	case 0:
		switch(value)
		{
		case 0: return("Stat,Separate");
		case 1: return("Stat,Right Sl");
		case 2:	return("Separate");
		case 3:	return("Right Slave");
		}
		break;
	case 1:
	case 2:
		sprintf(txt,"%.0f",(float)(value-0xfffe/2));
		break;
	case 3:
		sprintf(txt,"%d %s",value,DescribeValue(4,valInertiaUnit));
		break;
	case 4:
		switch(value)
		{
		case 0: return("ticks");
		case 1: return("ticks/256");
		case 2:	return("samples");
		case 3:	return("ms");
		case 4: return("secs");
		}
		break;
	}

	return txt;
}

void mi::MDKInit(CMachineDataInput * const pi)
{
	valMode=paraMode.DefValue;
	l_dc=(float)(paraLDC.DefValue-0xfffe/2);
	r_dc=(float)(paraRDC.DefValue-0xfffe/2);
	l_inertia=r_inertia=false;
	valInertia=paraInertia.DefValue;
	valInertiaUnit=paraInertiaUnit.DefValue;
	first=true;
	stereo_mode=false;
}

void mi::OutputModeChanged(bool stereo)
{
	stereo_mode=stereo;
	Command(1);
	if(r_inertia)
	{
		r_dc=r_dc_target;
		r_inertia=false;
	}
}

unsigned long mi::calculate_length(byte type, word len)
{
	unsigned long length;
	switch(type)
	{
	case 0:
		length=len*pMasterInfo->SamplesPerTick;
		break;
	case 1:
		length=(len*pMasterInfo->SamplesPerTick)/256;
		break;
	case 2:
		length=len;
		break;
	case 3:
		length=(len*pMasterInfo->SamplesPerSec)/1000;
		break;
	case 4:
		length=len*pMasterInfo->SamplesPerSec;
		break;
	}
	return length;
}


void mi::Tick()
{
  if (gval.mode != paraMode.NoValue)
  {
	if(valMode!=gval.mode)
	{
		if(r_inertia)
		{
			r_dc=r_dc_target;
			r_inertia=false;
		}
	}
	valMode=gval.mode;
  }
  if (gval.inertia != paraInertia.NoValue)
  {
	valInertia=gval.inertia;
  }
  if (gval.inertiaunit != paraInertiaUnit.NoValue)
  {
	valInertiaUnit=gval.inertiaunit;
  }

  if (gval.ldc != paraLDC.NoValue)
  {
	l_inertia=true;
	l_counter=calculate_length(valInertiaUnit,valInertia);
	l_dc_target=(float)(gval.ldc-0xfffe/2);
	l_dc_inc=(l_dc_target-l_dc)/l_counter;
  }
  if (gval.rdc != paraRDC.NoValue)
  {
	r_inertia=true;
	r_counter=calculate_length(valInertiaUnit,valInertia);
	r_dc_target=(float)(gval.rdc-0xfffe/2);
	r_dc_inc=(r_dc_target - r_dc)/r_counter;
  }

  // bypasses inertia on the first tick() to prevent the machine warmup effect
  // sets inertia to 0 so that the amp values are effective immediatly
  if(first)
  {
	r_inertia=true;
	r_counter=0;
	l_inertia=true;
	l_counter=0;
    first=false;
  }

} 


bool mi::MDKWorkStereo(float *psamples, int numsamples, int const mode)
{
	if ((mode==WM_WRITE)||(mode==WM_NOIO))
	{
		return false;
	}

	if (mode == WM_READ)		// <thru>
		return true;

	if(valMode>=2) // no stats
	{
	if(valMode==3) // linked
	{
	do 
	{
		if(l_inertia)
		{
			l_dc+=l_dc_inc;
			if(!(l_counter--))
			{
				l_inertia=false;
				l_dc=l_dc_target;
			}
		}
		psamples[0]+=l_dc;
		psamples[1]+=l_dc;
		psamples+=2;
	} while(--numsamples);
	} else	// separate
	{

	do 
	{
		if(l_inertia)
		{
			l_dc+=l_dc_inc;
			if(!(l_counter--))
			{
				l_inertia=false;
				l_dc=l_dc_target;
			}
		}
		psamples[0]+=l_dc;
		if(r_inertia)
		{
			r_dc+=r_dc_inc;
			if(!(r_counter--))
			{
				r_inertia=false;
				r_dc=r_dc_target;
			}
		}
		psamples[1]+=r_dc;
		psamples+=2;
	} while(--numsamples);
	}
	} else // stats
	{
	if(valMode==1) // linked
	{
	do 
	{
		if(l_inertia)
		{
			l_dc+=l_dc_inc;
			if(!(l_counter--))
			{
				l_inertia=false;
				l_dc=l_dc_target;
			}
		}
		left_dcsum_in+=psamples[0];
		psamples[0]+=l_dc;
		left_dcsum_out+=psamples[0];
		left_dcsum_in+=psamples[1];
		psamples[1]+=l_dc;
		left_dcsum_out+=psamples[1];
		dc_counter+=2;
		psamples+=2;
	} while(--numsamples);
	} else // separate
	{

	do 
	{
		if(l_inertia)
		{
			l_dc+=l_dc_inc;
			if(!(l_counter--))
			{
				l_inertia=false;
				l_dc=l_dc_target;
			}
		}
		left_dcsum_in+=psamples[0];
		if(psamples[0]>left_maxlevelfound)
		{
			left_maxlevelfound=psamples[0];
		}
		if(psamples[0]<left_minlevelfound)
		{
			left_minlevelfound=psamples[0];
		}
		psamples[0]+=l_dc;
		left_dcsum_out+=psamples[0];
		if(r_inertia)
		{
			r_dc+=r_dc_inc;
			if(!(r_counter--))
			{
				r_inertia=false;
				r_dc=r_dc_target;
			}
		}
		right_dcsum_in+=psamples[1];
		if(psamples[1]>right_maxlevelfound)
		{
			right_maxlevelfound=psamples[1];
		}
		if(psamples[1]<right_minlevelfound)
		{
			right_minlevelfound=psamples[1];
		}
		psamples[1]+=r_dc;
		right_dcsum_out+=psamples[1];
		dc_counter++;
		psamples+=2;
	} while(--numsamples);
	}
	}

	return true;
}

bool mi::MDKWork(float *psamples, int numsamples, int const mode)
{
	if ((mode==WM_WRITE)||(mode==WM_NOIO))
	{
		return false;
	}

	if (mode == WM_READ)		// <thru>
		return true;

	if(valMode>1) // no stats
	{
	do 
	{
		if(l_inertia)
		{
			l_dc+=l_dc_inc;
			if(!(l_counter--))
			{
				l_inertia=false;
				l_dc=l_dc_target;
			}
		}
		psamples[0]+=l_dc;
		psamples++;
	} while(--numsamples);
	} else // stats
	{
	do 
	{
		if(l_inertia)
		{
			l_dc+=l_dc_inc;
			if(!(l_counter--))
			{
				l_inertia=false;
				l_dc=l_dc_target;
			}
		}
		left_dcsum_in+=psamples[0];
		if(psamples[0]>left_maxlevelfound)
		{
			left_maxlevelfound=psamples[0];
		}
		if(psamples[0]<left_minlevelfound)
		{
			left_minlevelfound=psamples[0];
		}
		psamples[0]+=l_dc;
		left_dcsum_out+=psamples[0];
		dc_counter++;
		psamples++;
	} while(--numsamples);
	}

	return true;
}
