/*
xsample - extended sample objects for Max/MSP and pd (pure data)

Copyright (c) 2001-2005 Thomas Grill (gr@grrrr.org)
For information on usage and redistribution, and for a DISCLAIMER OF ALL
WARRANTIES, see the file, "license.txt," in this distribution.  
*/

#ifdef _MSC_VER
#define _USE_MATH_DEFINES
#endif

#include "main.h"
#include <math.h>
#include <stdio.h>

#ifdef _MSC_VER
#pragma warning (disable:4244)
#endif

#ifndef M_PI
#define M_PI 3.141592653589793238462643383
#endif

#define XZONE_TABLE 512


class xgroove:
	public xinter
{
	FLEXT_HEADER_S(xgroove,xinter,setup)

public:
	xgroove(int argc,const t_atom *argv);
	virtual ~xgroove();

	void m_pos(float pos)
    {
	    setpos(s2u?pos/s2u:0);
        Update(xsc_pos,true);
    }

    inline void m_posmod(float pos) { setposmod(pos?pos/s2u:0); } // motivated by Tim Blechmann

	inline void mg_pos(float &v) const { v = curpos*s2u; }


	enum xs_fade {
		xsf_keeplooppos = 0,xsf_keeplooplen,xsf_keepfade,xsf_inside
	};

	enum xs_shape {
		xss_lin = 0,xss_qsine,xss_hsine
	};


	void ms_xfade(int xf)
    { 
        if(xf < 0 || xf > xsf_inside) xf = xsf_keeplooppos;
	    xfade = (xs_fade)xf;
        Update(xsc_fade,true);
    }

    void ms_xshape(int sh);

	void ms_xzone(float xz);
	void mg_xzone(float &xz) { xz = _xzone*s2u; }

	void m_loop(xs_loop lp)
    { 
	    loopmode = lp,bidir = 1;
        Update(xsc_loop,true);
    }
	
protected:

	double curpos;  // in samples
	float bidir; // +1 or -1

	float _xzone,xzone;
    long znsmin,znsmax;
	xs_fade xfade;
	int xshape;
	t_sample **znbuf;
	t_sample *znpos,*znmul,*znidx;
	int pblksz;

	inline void setpos(double pos)
	{
		if(pos < znsmin) curpos = znsmin;
		else if(pos > znsmax) curpos = znsmax;
		else curpos = pos;
	}

	inline void setposmod(double pos)
	{
        if(pos >= 0) 
            curpos = znsmin+fmod(pos,znsmax-znsmin);
        else 
            curpos = znsmax+fmod(pos,znsmax-znsmin);
	}

    virtual void DoReset();
    virtual void DoUpdate(unsigned int flags);

	virtual void CbSignal();

	virtual void m_help();
	virtual void m_print();

private:
	static void setup(t_classid c);

    //! return true if something has changed
	bool do_xzone();

	DEFSIGFUN(s_pos_off);
	DEFSIGFUN(s_pos_once);
	DEFSIGFUN(s_pos_loop);
	DEFSIGFUN(s_pos_loopzn);
	DEFSIGFUN(s_pos_bidir);

	DEFSIGCALL(posfun);
	DEFSTCALL(zonefun);

	static t_sample fade_lin[],fade_qsine[],fade_hsine[];

	FLEXT_CALLBACK_F(m_pos)
	FLEXT_CALLBACK_F(m_posmod)
	FLEXT_CALLBACK_F(m_min)
	FLEXT_CALLBACK_F(m_max)
	FLEXT_CALLBACK(m_all)

    FLEXT_CALLSET_E(m_loop,xs_loop)

	FLEXT_CALLSET_I(ms_xfade)
	FLEXT_ATTRGET_I(xfade)
	FLEXT_CALLSET_I(ms_xshape)
	FLEXT_ATTRGET_I(xshape)
	FLEXT_CALLSET_F(ms_xzone)
	FLEXT_CALLGET_F(mg_xzone)

	FLEXT_CALLVAR_F(mg_pos,m_pos)
	FLEXT_CALLSET_F(m_min)
	FLEXT_CALLSET_F(m_max)
};


FLEXT_LIB_DSP_V("xgroove~",xgroove) 


t_sample xgroove::fade_lin[XZONE_TABLE+1];
t_sample xgroove::fade_qsine[XZONE_TABLE+1];
t_sample xgroove::fade_hsine[XZONE_TABLE+1];

void xgroove::setup(t_classid c)
{
	DefineHelp(c,"xgroove~");

	FLEXT_CADDMETHOD_(c,0,"all",m_all);
	FLEXT_CADDMETHOD(c,1,m_min);
	FLEXT_CADDMETHOD(c,2,m_max);

	FLEXT_CADDATTR_VAR(c,"min",mg_min,m_min); 
	FLEXT_CADDATTR_VAR(c,"max",mg_max,m_max);
	FLEXT_CADDATTR_VAR(c,"pos",mg_pos,m_pos);
	FLEXT_CADDMETHOD_(c,0,"posmod",m_posmod);

	FLEXT_CADDATTR_VAR_E(c,"loop",loopmode,m_loop);

	FLEXT_CADDATTR_VAR(c,"xfade",xfade,ms_xfade);
	FLEXT_CADDATTR_VAR(c,"xzone",mg_xzone,ms_xzone);
	FLEXT_CADDATTR_VAR(c,"xshape",xshape,ms_xshape);

	// initialize fade tables
	for(int i = 0; i <= XZONE_TABLE; ++i) {
		const float x = i*(1.f/XZONE_TABLE);
		// linear
		fade_lin[i] = x;

		// quarter sine wave
		fade_qsine[i] = sin(x*(M_PI/2));

		// half sine wave
		fade_hsine[i] = (sin(x*M_PI-M_PI/2)+1.f)*0.5f;
	}
}

xgroove::xgroove(int argc,const t_atom *argv):
	curpos(0),bidir(1),
	_xzone(0),xzone(0),
	xfade(xsf_keeplooppos),xshape(xss_lin),
	znpos(NULL),znmul(NULL),znidx(NULL),
	pblksz(0)
{
	int argi = 0;
#if FLEXT_SYS == FLEXT_SYS_MAX
	if(argc > argi && CanbeInt(argv[argi])) {
		outchns = GetAInt(argv[argi]);
		argi++;
	}
#endif

	if(argc > argi && IsSymbol(argv[argi])) {

        buf.Set(GetSymbol(argv[argi]),true);
		argi++;
		
#if FLEXT_SYS == FLEXT_SYS_MAX
		// old-style command line?
		if(argi == 1 && argc == 2 && CanbeInt(argv[argi])) {
			outchns = GetAInt(argv[argi]);
			argi++;
			post("%s: old style command line detected - please change to '%s [channels] [buffer]'",thisName(),thisName()); 
		}
#endif
	}

    AddInSignal("Signal of playing speed"); // speed signal
	AddInFloat("Starting point"); // min play pos
	AddInFloat("Ending point"); // max play pos
	for(int ci = 0; ci < outchns; ++ci) {
		char tmp[30];
		STD::sprintf(tmp,"Audio signal channel %i",ci+1); 
		AddOutSignal(tmp); // output
	}
	AddOutSignal("Position currently played"); // position
	AddOutFloat("Starting point (rounded to frame)"); // play min 
	AddOutFloat("Ending point (rounded to frame)"); // play max
	AddOutBang("Bang on loop end/rollover");  // loop bang
	
	// don't know vector size yet -> wait for m_dsp
	znbuf = new t_sample *[outchns];
	for(int i = 0; i < outchns; ++i) znbuf[i] = NULL;

    // initialize crossfade shape
    ms_xshape(xshape);
}

xgroove::~xgroove()
{
	if(znbuf) {
		for(int i = 0; i < outchns; ++i) if(znbuf[i]) FreeAligned(znbuf[i]); 
		delete[] znbuf;
	}

	if(znpos) FreeAligned(znpos);
	if(znidx) FreeAligned(znidx);
}

void xgroove::DoReset()
{
    xinter::DoReset();
	curpos = 0; 
	bidir = 1;
}

void xgroove::ms_xzone(float xz) 
{ 
	ChkBuffer(true);

	_xzone = (xz < 0 || !s2u)?0:xz/s2u; 
    Update(xsc_fade,true);
}

void xgroove::ms_xshape(int sh) 
{ 
    if(sh < 0 || sh > xss_hsine) sh = xss_lin;

	xshape = (xs_shape)sh;
	switch(xshape) {
		case xss_qsine: znmul = fade_qsine; break;
		case xss_hsine: znmul = fade_hsine; break;
		default:
			post("%s - shape parameter invalid, set to linear",thisName());
		case xss_lin: 
			znmul = fade_lin; break;
	}

	// no need to recalc the fade zone here
}


void xgroove::s_pos_off(int n,t_sample *const *invecs,t_sample *const *outvecs)
{
	t_sample *pos = outvecs[outchns];

	SetSamples(pos,n,curpos);
	
	playfun(n,&pos,outvecs); 

	SetSamples(pos,n,scale(curpos));
}

void xgroove::s_pos_once(int n,t_sample *const *invecs,t_sample *const *outvecs)
{
	const t_sample *speed = invecs[0];
	t_sample *pos = outvecs[outchns];
	bool lpbang = false;

	const double smin = curmin,smax = curmax,plen = smax-smin;

	if(plen > 0) {
		register double o = curpos;

		for(int i = 0; i < n; ++i) {	
			const t_sample spd = speed[i];  // must be first because the vector is reused for output!
			
			if(!(o < smax)) { o = smax; lpbang = true; }
			else if(o < smin) { o = smin; lpbang = true; }
			
			pos[i] = o;
			o += spd;
		}
		// normalize and store current playing position
		setpos(o);

		playfun(n,&pos,outvecs); 

		arrscale(n,pos,pos);
	} 
	else 
		s_pos_off(n,invecs,outvecs);
		
	if(lpbang) ToOutBang(outchns+3);
}

void xgroove::s_pos_loop(int n,t_sample *const *invecs,t_sample *const *outvecs)
{
	const t_sample *speed = invecs[0];
	t_sample *pos = outvecs[outchns];
	bool lpbang = false;

#ifdef __VEC__
	// prefetch cache
	vec_dst(speed,GetPrefetchConstant(1,n>>2,0),0);
#endif

	const double smin = curmin,smax = curmax,plen = smax-smin;

	if(plen > 0) {
		register double o = curpos;

        if(wrap && smin < 0 && smax >= buf.Frames()) {
		    for(int i = 0; i < n; ++i) {	
			    const t_sample spd = speed[i];  // must be first because the vector is reused for output!

			    // normalize offset
			    if(!(o < smax)) {  // faster than o >= smax
				    o = fmod(o-smin,plen)+smin;
				    lpbang = true;
			    }
			    else if(o < smin) {
				    o = fmod(o-smin,plen)+smax; 
				    lpbang = true;
			    }

                // TODO normalize to 0...buf.Frames()
			    pos[i] = o;
			    o += spd;
		    }
        }
        else {
		    for(int i = 0; i < n; ++i) {	
			    const t_sample spd = speed[i];  // must be first because the vector is reused for output!

			    // normalize offset
			    if(!(o < smax)) {  // faster than o >= smax
				    o = fmod(o-smin,plen)+smin;
				    lpbang = true;
			    }
			    else if(o < smin) {
				    o = fmod(o-smin,plen)+smax; 
				    lpbang = true;
			    }

			    pos[i] = o;
			    o += spd;
		    }
        }

		// normalize and store current playing position
		setpos(o);

		playfun(n,&pos,outvecs); 

		arrscale(n,pos,pos);
	} 
	else 
		s_pos_off(n,invecs,outvecs);
		
#ifdef __VEC__
	vec_dss(0);
#endif

	if(lpbang) ToOutBang(outchns+3);
}

void xgroove::s_pos_loopzn(int n,t_sample *const *invecs,t_sample *const *outvecs)
{
	const t_sample *speed = invecs[0];
	t_sample *pos = outvecs[outchns];
	bool lpbang = false;

	FLEXT_ASSERT(xzone);

	const float xz = xzone,xf = (float)XZONE_TABLE/xz;

    // adapt the playing bounds to the current cross-fade zone
    const long smin = znsmin,smax = znsmax,plen = smax-smin;

	// temporary storage
	const long cmin = curmin,cmax = curmax;
	// hack -> set curmin/curmax to loop extremes so that sampling functions (playfun) don't get confused
	curmin = smin,curmax = smax;

	if(plen > 0) {
		bool inzn = false;
		register double o = curpos;

		// calculate inner cross-fade boundaries
		const double lmin = smin+xz,lmax = smax-xz,lsh = lmax-lmin+xz;
		const double lmin2 = lmin-xz/2,lmax2 = lmax+xz/2;

 		for(int i = 0; i < n; ++i) {	
			// normalize offset
            if(o < smin) {
				o = fmod(o-smin,plen)+smax; 
				lpbang = true;
			}
			else if(!(o < smax)) {
				o = fmod(o-smin,plen)+smin;
				lpbang = true;
			}

			if(o < lmin) {
				register float inp;
				if(o < lmin2) {
					// in first half of early cross-fade zone
					// this happens only once, then the offset is normalized to the end
					// of the loop (before mid of late crossfade)

					o += lsh;
					// now lmax <= o <= lmax2
					lpbang = true;

					inp = xz-(float)(o-lmax);  // 0 <= inp < xz
					znpos[i] = lmin-inp;
				}
				else { 
					// in second half of early cross-fade zone

					inp = xz+(float)(o-lmin);  // 0 <= inp < xz
					znpos[i] = lmax+inp;
				}
				znidx[i] = inp*xf;
				inzn = true;
			}
			else if(!(o < lmax)) {
				register float inp;
				if(!(o < lmax2)) {
					// in second half of late cross-fade zone
					// this happens only once, then the offset is normalized to the beginning
					// of the loop (after mid of early crossfade)
					o -= lsh;
					// now lmin2 <= o <= lmin
					lpbang = true;

					inp = xz+(float)(o-lmin);  // 0 <= inp < xz
					znpos[i] = lmax+inp;
				}
				else { 
					// in first half of late cross-fade zone
					inp = xz-(float)(o-lmax);  // 0 <= inp < xz
					znpos[i] = lmin-inp;
				}
				znidx[i] = inp*xf;
				inzn = true;
			}
			else
				znidx[i] = XZONE_TABLE,znpos[i] = 0;

			const t_sample spd = speed[i];  // must be first because the vector is reused for output!
			pos[i] = o;
			o += spd;
		}

		// normalize and store current playing position
		setpos(o);

		// calculate samples (1st voice)
		playfun(n,&pos,outvecs); 

		// rescale position vector
		arrscale(n,pos,pos);

		if(inzn) {
			// only if we have touched the cross-fade zone
			
			// calculate samples in loop zone (2nd voice)
			playfun(n,&znpos,znbuf); 
			
			// calculate counterpart in loop fade
			arrscale(n,znidx,znpos,XZONE_TABLE,-1);
			
			// calculate fade coefficients by sampling from the fade curve
			zonefun(znmul,0,XZONE_TABLE+1,n,1,1,&znidx,&znidx,false);
			zonefun(znmul,0,XZONE_TABLE+1,n,1,1,&znpos,&znpos,false);

			// mix voices for all channels
			for(int o = 0; o < outchns; ++o) {
				MulSamples(outvecs[o],outvecs[o],znidx,n);
				MulSamples(znbuf[o],znbuf[o],znpos,n);
				AddSamples(outvecs[o],outvecs[o],znbuf[o],n);
			}
		}
	} 
	else 
		s_pos_off(n,invecs,outvecs);
		
	curmin = cmin,curmax = cmax;

	if(lpbang) ToOutBang(outchns+3);
}

void xgroove::s_pos_bidir(int n,t_sample *const *invecs,t_sample *const *outvecs)
{
	const t_sample *speed = invecs[0];
	t_sample *pos = outvecs[outchns];
	bool lpbang = false;

	const int smin = curmin,smax = curmax,plen = smax-smin;

	if(plen > 0) {
		register double o = curpos;
		register float bd = bidir;

		for(int i = 0; i < n; ++i) {	
			const t_sample spd = speed[i];  // must be first because the vector is reused for output!

			// normalize offset
            // \todo at the moment fmod doesn't take bidirectionality into account!!
			if(!(o < smax)) {
				o = smax-fmod(o-smax,plen); // mirror the position at smax
				bd = -bd;
				lpbang = true;
			}
			else if(o < smin) {
				o = smin+fmod(smin-o,plen); // mirror the position at smin
				bd = -bd;
				lpbang = true;
			}

			pos[i] = o;
			o += spd*bd;
		}
		// normalize and store current playing position
		setpos(o);

		bidir = bd;
		playfun(n,&pos,outvecs); 

		arrscale(n,pos,pos);
	} 
    else
		s_pos_off(n,invecs,outvecs);
		
	if(lpbang) ToOutBang(outchns+3);
}

void xgroove::CbSignal() 
{ 
    int ret = ChkBuffer(true);

    if(ret) {
        FLEXT_ASSERT(buf.Valid());
        
        const lock_t l = Lock();
		posfun(Blocksize(),InSig(),OutSig()); 
        Unlock(l);

        Refresh();
    }
	else
		zerofun(Blocksize(),InSig(),OutSig());
}


void xgroove::DoUpdate(unsigned int flags)
{
    xinter::DoUpdate(flags);

    if(flags&xsc_range) {
        // output new range
	    ToOutFloat(outchns+1,curmin*s2u);
	    ToOutFloat(outchns+2,curmax*s2u);
    }

    if(flags&(xsc_fade|xsc_range))
        if(do_xzone()) flags |= xsc_play;

    if(flags&(xsc_pos|xsc_range))
        // normalize position
        setpos(curpos);

    // loop zone must already be set
    if(flags&xsc_play) {
	    if(doplay) {
		    switch(loopmode) {
		    case xsl_once: 
			    SETSIGFUN(posfun,SIGFUN(s_pos_once)); 
			    break;
		    case xsl_loop: 
			    if(xzone > 0) {
				    const int blksz = Blocksize();

				    if(pblksz != blksz) {
					    for(int o = 0; o < outchns; ++o) {
						    if(znbuf[o]) FreeAligned(znbuf[o]); 
						    znbuf[o] = (t_sample *)NewAligned(blksz*sizeof(t_sample)); 
					    }
    				
					    if(znpos) FreeAligned(znpos); 
					    znpos = (t_sample *)NewAligned(blksz*sizeof(t_sample));
					    if(znidx) FreeAligned(znidx);
					    znidx = (t_sample *)NewAligned(blksz*sizeof(t_sample));

					    pblksz = blksz;
				    }

				    SETSIGFUN(posfun,SIGFUN(s_pos_loopzn)); 

				    // linear interpolation should be just ok for fade zone, no?
				    switch(outchns) {
					    case 1:	SETSTFUN(zonefun,TMPLSTF(st_play2,1,1)); break;
					    case 2:	SETSTFUN(zonefun,TMPLSTF(st_play2,1,2)); break;
					    case 4:	SETSTFUN(zonefun,TMPLSTF(st_play2,1,4)); break;
					    default: SETSTFUN(zonefun,TMPLSTF(st_play2,1,-1));
				    }
			    }
			    else
				    SETSIGFUN(posfun,SIGFUN(s_pos_loop)); 
			    break;
		    case xsl_bidir: 
			    SETSIGFUN(posfun,SIGFUN(s_pos_bidir)); 
			    break;
            default: ; // just to prevent warning
		    }
	    }
	    else
		    SETSIGFUN(posfun,SIGFUN(s_pos_off));
    }
}

bool xgroove::do_xzone()
{
	// \todo do we really need this?
	if(!s2u) return false; // this can happen if DSP is off 

    const long frames = buf.Frames();
    if(!frames) return false;

	xzone = _xzone; // make a copy for changing it

	if(xfade == xsf_inside) { 
		// fade zone goes inside the loop -> loop becomes shorter

		// \todo what about round-off?
		const long maxfd = (curmax-curmin)/2;
		if(xzone > maxfd) xzone = maxfd;

		znsmin = curmin,znsmax = curmax;
	}
	else if(xfade == xsf_keepfade) { 
		// try to keep fade zone
		// change of loop bounds may happen

		// restrict xzone to half of buffer
		const long maxfd = frames/2;
		if(xzone > maxfd) xzone = maxfd;

		// \todo what about round-off?
        const long hzone = CASTINT<long>(xzone/2.f+0.5f);
		znsmin = curmin-hzone;
		znsmax = curmax+hzone;

		// widen loop if xzone doesn't fit into it
		// \todo check formula
		long lack = CASTINT<long>(ceil((xzone*2.f-(znsmax-znsmin))/2.f));
		if(lack > 0) znsmin -= lack,znsmax += lack;

        if(!wrap) {
		    // check buffer limits and shift bounds if necessary
		    if(znsmin < 0) {
			    znsmax -= znsmin;
			    znsmin = 0;
		    }
		    if(znsmax > frames) 
                znsmax = frames;
        }
	}
	else if(xfade == xsf_keeplooplen) { 
		// try to keep loop length
		// shifting of loop bounds may happen

		const long plen = curmax-curmin;
		if(xzone > plen) xzone = plen;
		const long maxfd = frames-plen;
		if(xzone > maxfd) xzone = maxfd;

		// \todo what about round-off?
        const long hzone = CASTINT<long>(xzone/2.f+0.5f);
		znsmin = curmin-hzone;
		znsmax = curmax+hzone;

        if(!wrap) {
		    // check buffer limits and shift bounds if necessary
		    // both cases can't happen because of xzone having been limited above
		    if(znsmin < 0) {
			    znsmax -= znsmin;
			    znsmin = 0;
		    }
		    else if(znsmax > frames) {
			    znsmin -= znsmax-frames;
			    znsmax = frames;
		    }
        }
	}
	else if(xfade == xsf_keeplooppos) { 
		// try to keep loop position and length

		// restrict fade zone to maximum length 
		const long plen = curmax-curmin;
		if(xzone > plen) xzone = plen;

		// \todo what about round-off?
        const long hzone = CASTINT<long>(xzone/2.f+0.5f);
		znsmin = curmin-hzone;
		znsmax = curmax+hzone;

		long ovr = znsmax-frames;
		if(-znsmin > ovr) ovr = -znsmin;
		if(ovr > 0) {
			znsmin += ovr;
			znsmax -= ovr;
			xzone -= ovr*2;
		}
	}

	FLEXT_ASSERT(znsmin <= znsmax && (znsmax-znsmin) >= xzone*2.f);

    return true;
}


void xgroove::m_help()
{
	post("%s - part of xsample objects, version " XSAMPLE_VERSION,thisName());
	post("(C) Thomas Grill, 2001-2005");
#if FLEXT_SYS == FLEXT_SYS_MAX
	post("Arguments: %s [channels=1] [buffer]",thisName());
#else
	post("Arguments: %s [buffer]",thisName());
#endif
	post("Inlets: 1:Messages/Speed signal, 2:Min position, 3:Max position");
	post("Outlets: 1:Audio signal, 2:Position signal, 3:Min position (rounded), 4:Max position (rounded)");	
	post("Methods:");
	post("\thelp: shows this help");
	post("\tset [name] / @buffer [name]: set buffer or reinit");
	post("\tenable 0/1: turn dsp calculation off/on");	
	post("\treset: reset min/max playing points and playing offset");
	post("\tprint: print current settings");
	post("\t@loop 0/1/2: sets looping to off/forward/bidirectional");
	post("\t@interp 0/1/2: set interpolation to off/4-point/linear");
	post("\t@min {unit}: set minimum playing point");
	post("\t@max {unit}: set maximum playing point");
	post("\tall: select entire buffer length");
	post("\tpos {unit}: set playing position (obeying the current scale mode)");
	post("\tposmod {unit}: set playing position (modulo into min/max range)");
	post("\tbang/start: start playing");
	post("\tstop: stop playing");
	post("\trefresh: checks buffer and refreshes outlets");
	post("\t@units 0/1/2/3: set units to frames/buffer size/ms/s");
	post("\t@sclmode 0/1/2/3: set range of position to units/units in loop/buffer/loop");
	post("\t@xzone {unit}: length of loop crossfade zone");
	post("\t@xfade 0/1/2/3: fade mode (keep loop/keep loop length/keep fade/inside loop)");
	post("\t@xshape 0/1/2: shape of crossfade (linear/quarter sine/half sine)");
	post("");
} 

void xgroove::m_print()
{
	static const char *sclmode_txt[] = {"units","units in loop","buffer","loop"};
	static const char *interp_txt[] = {"off","4-point","linear"};
	static const char *loop_txt[] = {"once","looping","bidir"};

	// print all current settings
	post("%s - current settings:",thisName());
	post("bufname = '%s', length = %.3f, channels = %i",buf.Name(),(float)(buf.Frames()*s2u),buf.Channels()); 
	post("out channels = %i, frames/unit = %.3f, scale mode = %s",outchns,(float)(1./s2u),sclmode_txt[sclmode]); 
	post("loop = %s, interpolation = %s",loop_txt[(int)loopmode],interp_txt[interp >= xsi_none && interp <= xsi_lin?interp:xsi_none]); 
	post("loop crossfade zone = %.3f",(float)(xzone*s2u)); 
	post("");
}