// 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:
  1.04	Added new statistics item: "Average power"
  1.03  Noticed an error when suggesting volumes when the signal
		is really loud: the machine would suggest values that are
		not in the range of the volume faders. "Please set master
		volume to 21211 [129.35%]."
		Added new statistics item: "Max change", "Max change location"
  1.02	Got a little guilty consciousness because of not obeying
    the WM_READ mode. <thru> now bypasses machine.
  1.01	Fixed zero crossings miscalculation and added some
	more info. Thanks to Tamas for reporting.
  1.0	First release

*/

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

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

// Machine's info

CMachineInfo const MacInfo = 
{
	MT_EFFECT,MI_VERSION,MIF_DOES_INPUT_MIXING,miMIN_TRACKS,miMAX_TRACKS,
	miNUMGLOBALPARAMETERS,miNUMTRACKPARAMETERS,NULL,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 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;

private:
	void print_amp(char *txt, float num);
	void print_ana1(char *txt, float max,float min);
	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, unsigned long time_maxvalue, unsigned long time_minvalue, double dcsum,double rms,unsigned long zero_cross, double max_change, unsigned long change_time, double avg);

	float left_maxlevelfound,left_minlevelfound;
	float right_maxlevelfound,right_minlevelfound;
	double left_dcsum,right_dcsum;
	double left_rms,right_rms;
	double left_avg,right_avg;
	float prev_left,prev_right;
	unsigned long left_zerocross, right_zerocross;
	unsigned long dcnum;
	unsigned long time_left_minvalue,time_left_maxvalue,time_right_minvalue,time_right_maxvalue;
	bool stereo_mode;
	double left_max_change, right_max_change;
	unsigned long time_left_change,time_right_change;
};


DLL_EXPORTS

mi::mi()
{
	GlobalVals = NULL;
	TrackVals = NULL;
	AttrVals = NULL;

}

// 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)
{
	float tmp_max,tmp_maxrange,tmp;
	if(max>32767.0||min<-32768.0)
	{
		if(fabs(max)<fabs(min))
		{
			tmp_max=(float)fabs(min);
			tmp_maxrange=32768.0;
		} else
		{
			tmp_max=(float)fabs(max);
			tmp_maxrange=32767.0;
		}
		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);
		tmp=(float)16384.0*(tmp_maxrange/tmp_max);
		if((int)tmp)
		{
		  sprintf(txt,"%sThe input volume slider of the next machine should be set to under %d [%.2f%%].\n",txt,(int)tmp,(100*tmp)/16384 );
		} else
		{
		// the setting would be out of the range of the slider, print a warning
		sprintf(txt,"%sThe input volume slider of the next machine CAN NOT BE SET to a value that would prevent clipping.\n -> Set it as small as possible (0 mutes the signal).\n",txt,(int)tmp,(100*tmp)/16384 );
		}
		sprintf(txt,"%s",txt);
		tmp=(float)(log10(tmp_maxrange/tmp_max)*((20.0*16384.0)/(-80.0)));
		if(tmp<16384)
		{
		sprintf(txt,"%sThe master output volume slider should have a value over %d [%.2f%%] to prevent clipping.\n",txt,(int)tmp,(100*tmp)/16384);
		} else
		{
		// the setting would be out of the range of the slider, print a warning
		sprintf(txt,"%sThe master output volume slider CAN NOT BE SET to a value that would prevent clipping.\n -> Set it as high as possible (16384 mutes the output).\n",txt,(int)tmp,(100*tmp)/16384);
		}
	} 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_channel(char *txt,float maxlevelfound, float minlevelfound, unsigned long time_maxvalue, unsigned long time_minvalue, double dcsum,double rms,unsigned long zero_cross, double max_change, unsigned long change_time, double avg)
{
	sprintf(txt,"%sMax ",txt);
	print_amp(txt,maxlevelfound);
    sprintf(txt,"%sMax value location: at",txt);
    print_time(txt,time_maxvalue);

	sprintf(txt,"%sMin ",txt);
	print_amp(txt,minlevelfound);
    sprintf(txt,"%sMin value location: at",txt);
    print_time(txt,time_minvalue);

	sprintf(txt,"%sDC Offset (average): %.1f (",txt,dcsum/dcnum);
	if(dcsum/dcnum)
	{
	  sprintf(txt,"%s%.1fdB; %+.2f%%)\n",txt,(float)(20.0*log10(fabs(dcsum/(dcnum*32768.0)))),(float)((100*(dcsum/dcnum))/32768.0));
	} else
	{
	  sprintf(txt,"%s-inf dB; 0.00%%)\n",txt);
	}
	rms=sqrt(rms/dcnum);
	sprintf(txt,"%sRMS Power: %.1f (",txt,rms);
	if(rms)
	{
	  sprintf(txt,"%s%.1fdB; %+.2f%%)\n",txt,(float)(20.0*log10(rms/32768.0)),(float)((100*rms)/32768.0));
	} else
	{
	  sprintf(txt,"%s-inf dB; 0.00%%)\n",txt);
	}
	avg=avg/dcnum;
	sprintf(txt,"%sAverage Power: %.1f (",txt,avg);
	if(rms)
	{
	  sprintf(txt,"%s%.1fdB; %+.2f%%)\n",txt,(float)(20.0*log10(avg/32768.0)),(float)((100*avg)/32768.0));
	} else
	{
	  sprintf(txt,"%s-inf dB; 0.00%%)\n",txt);
	}
	sprintf(txt,"%sZero crossings: %ld (%.2fHz)\n",txt,zero_cross,(float)(zero_cross*pMasterInfo->SamplesPerSec)/(dcnum*2));
	sprintf(txt,"%sMax change: %.1f (",txt,max_change);
	if(rms)
	{
	  sprintf(txt,"%s%.1fdB; %+.2f%%)\n",txt,(float)(20.0*log10(max_change/32768.0)),(float)((100*max_change)/32768.0));
      sprintf(txt,"%sMax change location: at",txt);
	  print_time(txt,change_time);
	} else
	{
	  sprintf(txt,"%s-inf dB; 0.00%%)\n",txt);
	}
}

void mi::Command(int const i)
{
	static char txt[2000];
	switch(i)
	{
	case 0:
		if(dcnum)
		{
		  sprintf(txt,"Sound analysis:\n\nStatistical data collected:");
		  print_time(txt,dcnum);
		  if(stereo_mode)
		  {
			  sprintf(txt,"%s\nLeft channel:\n\n",txt);
			  print_channel(txt,left_maxlevelfound,left_minlevelfound,time_left_maxvalue, time_left_minvalue, left_dcsum,left_rms,left_zerocross,left_max_change,time_left_change,left_avg);
			  sprintf(txt,"%s\nRight channel:\n\n",txt);
			  print_channel(txt,right_maxlevelfound,right_minlevelfound,time_right_maxvalue, time_right_minvalue, right_dcsum,right_rms,right_zerocross,right_max_change,time_right_change,right_avg);
			  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,time_left_maxvalue, time_left_minvalue, left_dcsum,left_rms,left_zerocross,left_max_change,time_left_change,left_avg);
			  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=right_dcsum=0.0;
		left_rms=right_rms=0.0;
		left_avg=right_avg=0.0;
		dcnum=0;
		time_left_minvalue=time_left_maxvalue=time_right_minvalue=time_right_maxvalue=0;
		prev_left=prev_right=0.0;
		left_zerocross=right_zerocross=0;
		left_max_change=right_max_change=0.0;
		time_left_change=time_right_change=0;
		break;
	case 2:
		pCB->MessageBox(miMACHINE_NAME"\n\nBuild date: "__DATE__"\nVersion: "miVERSION"\nCoded by: "miMACHINE_AUTHOR"\nThanks to Oskari and other #buzz developers.\n\nCheck out http://www.iki.fi/apo/buzz/\nfor more buzz stuff.\n\nExcellent skin made by Hymax.");
		break;
	}
}

void mi::MDKInit(CMachineDataInput * const pi)
{
	stereo_mode=false;
	Command(1);
}

void mi::OutputModeChanged(bool stereo)
{
	stereo_mode=stereo;
	Command(1);
}

bool mi::MDKWorkStereo(float *psamples, int numsamples, int const mode)
{
	float l,r;
	double tmp;

	if ((mode==WM_WRITE)||(mode==WM_NOIO))
	{
		return false;
	}

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

	do 
	{
		l=psamples[0];
		r=psamples[1];
		psamples+=2;
		left_dcsum+=l;
		left_rms+=l*l;
		left_avg+=fabs(l);
		if(l>left_maxlevelfound)
		{
			left_maxlevelfound=l;
			time_left_maxvalue=dcnum;
		}
		if(l<left_minlevelfound)
		{
			left_minlevelfound=l;
			time_left_minvalue=dcnum;
		}
		if(prev_left>=0)
		{
			if(l<0)
			left_zerocross++;
		} else
		{
			if(l>=0)
			left_zerocross++;
		}
		tmp=fabs(l-prev_left);
		if(tmp>left_max_change)
		{
			left_max_change=tmp;
			time_left_change=dcnum;
		}
		prev_left=l;

		right_dcsum+=r;
		right_rms+=r*r;
		right_avg+=fabs(r);
		if(r>right_maxlevelfound)
		{
			right_maxlevelfound=r;
			time_right_maxvalue=dcnum;
		}
		if(r<right_minlevelfound)
		{
			right_minlevelfound=r;
			time_right_minvalue=dcnum;
		}
		if(prev_right>=0)
		{
			if(r<0)
			right_zerocross++;
		} else
		{
			if(r>=0)
			right_zerocross++;
		}
		tmp=fabs(r-prev_right);
		if(tmp>right_max_change)
		{
			right_max_change=tmp;
			time_right_change=dcnum;
		}
		prev_right=r;
		dcnum++;
	} while(--numsamples);
	return true;
}

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

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

	do 
	{
		l=*psamples;
		psamples++;
		left_dcsum+=l;
		left_rms+=l*l;
		left_avg+=fabs(l);
		if(l>left_maxlevelfound)
		{
			left_maxlevelfound=l;
			time_left_maxvalue=dcnum;
		}
		if(l<left_minlevelfound)
		{
			left_minlevelfound=l;
			time_left_minvalue=dcnum;
		}
		if(prev_left>=0)
		{
			if(l<0)
			left_zerocross++;
		} else
		{
			if(l>=0)
			left_zerocross++;
		}
		tmp=fabs(l-prev_left);
		if(tmp>left_max_change)
		{
			left_max_change=tmp;
			time_left_change=dcnum;
		}
		prev_left=l;
		dcnum++;
	} while(--numsamples);
	return true;
}
