/* --------------------------- beat  ------------------------------------------ */
/*                                                                              */
/* Detect the beats per minute of a MIDI stream.                                */
/* Written by Olaf Matthes (olaf.matthes@gmx.de)                                */
/* Based on code written by Robert Rowe.                                        */
/* Get source at http://www.akustische-kunst.org/puredata/maxlib/               */
/*                                                                              */
/* This program is free software; you can redistribute it and/or                */
/* modify it under the terms of the GNU General Public License                  */
/* as published by the Free Software Foundation; either version 2               */
/* of the License, or (at your option) any later version.                       */
/*                                                                              */
/* This program is distributed in the hope that it will be useful,              */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of               */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                */
/* GNU General Public License for more details.                                 */
/*                                                                              */
/* You should have received a copy of the GNU General Public License            */
/* along with this program; if not, write to the Free Software                  */
/* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.  */
/*                                                                              */
/* Based on PureData by Miller Puckette and others.                             */
/*                                                                              */
/* ---------------------------------------------------------------------------- */

#include "m_pd.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BEAT_LONG 1500			/* longest time we take into concideration (40 bpm) */
#define BEAT_SHORT 300			/* shortest time we take into concideration (200 bpm) */

static char *version = "beat v0.1, written by Olaf Matthes <olaf.matthes@gmx.de>";

typedef struct
{
	t_int	points;					/* number of points assigned to this theory */
	double  expect;					/* time of next expected hit */
	t_int	onbeat;					/* whether (1) or not (0) it was on the beat */
} beat_theory;

typedef struct						/* used for sorting theories */
{
	t_int points;
	t_int theory;
} beat_sort_record;

 
typedef struct beat
{
  t_object x_ob;
  t_clock  *x_clock;
  t_outlet *x_outbpm;               /* beat as MIDI note number */
  t_outlet *x_outms;                /* beat in milliseconds */
  t_outlet *x_outbeat;				/* send a bang whenever beat is 'on beat' */
  t_int    x_print;                 /* switch printing to console window on / off */

  t_int    x_num_beats;				/* number of beats we've received */
  double   x_beat_period;			/* time in ms until next expected beat / beat pulse */
  beat_theory	x_beats[BEAT_LONG];
  double   x_beatexpect;			/* expected time for next beat */
  t_int    x_on_beat;				/* indicate if last event was on beat */
  t_int    x_band_percent;

  t_int    x_pitch;
  t_int    x_velo;
	/* helpers needed to do the time calculations */
  double   x_this_input;
  double   x_last_input;
  double   x_lasttime;
  double   x_lastlasttime;
} t_beat;

/* ---------------- mathematical functions to work with doubles  -------------- */
static double double_abs(double value)
{
	if(value < 0)
		return (value * -1);
	else
		return (value);
}

/* --------------- beat  stuff ------------------------------------------------ */
	/* evaluate results: find theory that is the most likely one and
       print out internal data to console window if print is enabled */
static int beat_evaluate(t_beat *x)
{
	int i, j, K;
	char string[256];
	char info[40];
	beat_sort_record theories[BEAT_LONG], *sortp, R;
	int value;	/* the result of the sorting */

	for (i = 0; i < BEAT_LONG; i++)
	{		/* prepare sort records */
		sortp         = &(theories[i]);
		sortp->points = x->x_beats[i].points;
		sortp->theory = i;
	}
	for (j = 2; j < BEAT_LONG; j++)
	{		/* sort */
		i = j - 1;
		K = theories[j].points;
		R = theories[j];
		while (i > 0)
		{
			if (K >= theories[i].points)
			{
				theories[i+1] = R;
				break;
			}
			else
			{
				theories[i+1] = theories[i];
				i -= 1;
			}
		}
		if (i==0) theories[i+1] = R;
	}
		/* get leading result */
	sortp = &(theories[BEAT_LONG - 1]);
	value = sortp->theory;	/* get our resulting theory */

	if(x->x_print)
	{
		post("         0         1         2         3         4         R            E");
		*string = '\0';						/* print out five leading theories */
		sprintf(info, "%4g", x->x_this_input);
		strcat(string, info);
		for(i = 1; i < 6; i++)
		{
			sortp = &(theories[BEAT_LONG - i]);
			sprintf(info, " %4d[%3d]", (int) sortp->theory, (int) sortp->points);
			strcat(string, info);
		}
		sprintf(info, "  %g %g", clock_getlogicaltime(), x->x_beatexpect);
		strcat(string, info);
		post(string);
	}

	return value;
}

	/* reduce duration to fit into our processing window */
	/* some sort of 'double modulo'...                   */
static double beat_reduce_offset(double duration)
{
	double temp	= duration;
	int divisor = 2;					/* first try dividing by two */
	while (temp > BEAT_LONG)		    /* while duration is too long */
		temp = duration / divisor++;    /* divide by progressively higher divisors */
	return temp;				        /* return a value in bounds */
}

/*
 * beat_eligible: determine whether an event is eligible for consideration
 * as a beat theory
 */
static int beat_eligible(double candidate, int* offsets, int num_offsets)
{
	double diff;
	int i;

	if (candidate >= BEAT_LONG)		/* if too long try subharmonics */
		candidate = beat_reduce_offset(candidate);
	
		/* if candidate is close to one already found */
	for(i = 0; i < num_offsets; i++)
	{
		diff = double_abs((candidate - offsets[i]));
		if (diff < offsets[i]/20) {
			if (candidate > offsets[i])
				++offsets[i]; else			/* pull existing one */
			if (candidate < offsets[i])		/* toward new candidate */
				--offsets[i];
			return 0;						/* declare candidate ineligible */
		}
	}
	return candidate;						/* otherwise return legal candidate */
}

static void beat_float(t_beat *x, t_floatarg f)
{
	t_int velo = x->x_velo;
	int i, j, indx;
	int num_offsets, candidate;
	int low_limit, high_limit, width, deviation;
	int points, band, center_offset, period;
	beat_theory* t;
	int offsets[7];
	static int factors[10] = 
		{ 200, 50, 300, 150, 66, 400, 600, 133, 33, 75 };
	double now = clock_getlogicaltime();
	t_float outvalue;

	x->x_pitch = (t_int)f;
	x->x_this_input = clock_gettimesince(x->x_last_input);

	if(velo != 0)	/* note-on received */
	{
		if(++x->x_num_beats == 1)
		{
			goto time;					/* only one event, no beats yet */
		}

		num_offsets = 0;
		candidate  = beat_eligible(x->x_this_input, offsets, num_offsets);
		if(candidate)
			offsets[num_offsets++] = candidate;				/* offset[0] set to incoming offset */
 
		if(x->x_num_beats > 2)
		{								/* if three events */
				/* check previous for eligibility */
			candidate = beat_eligible(x->x_lasttime, offsets, num_offsets);
			if (candidate)
				offsets[num_offsets++] = candidate;
			candidate  = x->x_this_input + x->x_lasttime;	/* add current and previous offsets */
			candidate  = beat_eligible(candidate, offsets, num_offsets);
			if (candidate)									/* add to list if eligible */
				offsets[num_offsets++] = candidate;
		}

		if(x->x_num_beats > 3)
		{
			candidate  = beat_eligible(x->x_lastlasttime, offsets, num_offsets);
			if (candidate)
				offsets[num_offsets++] = candidate;
			candidate += x->x_lasttime;
			candidate  = beat_eligible(candidate, offsets, num_offsets);
			if (candidate)
				offsets[num_offsets++] = candidate;
		}

		indx = 0;
		for(i = num_offsets; i < 7; i++)
		{
			offsets[i] = 0;
			if (indx >= 10) break;
			candidate  = 0;
			while ((indx < 10) && (!candidate))
				candidate = beat_eligible((x->x_this_input * factors[indx++])/100, offsets, num_offsets);
			if (candidate)
				offsets[num_offsets++] = candidate;
		}
		
		for(i = 0; i < num_offsets; i++)
		{
			band = offsets[i] * x->x_band_percent / 100;
			if ((low_limit = offsets[i] - band) < 0)		/* add points in a critical band */
				low_limit  = 0;								/* around calculated offset */
			if ((high_limit = offsets[i] + band) > BEAT_LONG)
				high_limit = BEAT_LONG;
			center_offset = offsets[i];						/* midpoint of increment */
			points = 0;
			for (j = low_limit; j < high_limit; j++)
			{
				if ((points = x->x_beats[j].points) > 0)
				{		/* if there is already activation */
					deviation = j - center_offset;			/* record deviation from midpoint */
					x->x_beats[j].points = 0;
					if (deviation < 0) {					/* if there is activation below midpoint */
						t = &(x->x_beats[j+1]);				/* take theory one above prior */
					} else
					if (deviation > 0) {					/* if there is activation above midpoint */
						t = &(x->x_beats[j-1]);				/* take theory one below prior */
					} else
						t = &(x->x_beats[j]);				/* landed right on it */
					t->points = points + (num_offsets-i);
					break;
				}
			}
			if (!points)
				x->x_beats[center_offset].points = num_offsets - i;
		}

			/* boost hits, and suppress theories with missed beats */
		period = 0;
		points = 0;
		for (i = BEAT_SHORT; i < BEAT_LONG; i++)
		{
			t = &(x->x_beats[i]);
			width = 5 > (t->expect / 7) ? 5 : (t->expect / 7);
			t->expect -= x->x_this_input;
			t->onbeat  = 0;
			if(double_abs(t->expect) <= width)	/* lies within range */
			{
				t->expect = i;
				t->onbeat = 1;
	 			if (t->points > 0)
					t->points += 4;				/* add 4 points */
			}
			else if(t->expect < 0)
			{
				t->points -= 8;
				t->expect  = i;
			}
			if (t->points < 0)   t->points =   0; else
			if (t->points > 200) t->points = 200;
			if (t->points > points)
			{
				points = t->points;
				period = i;
			}
		}



		x->x_beat_period    = (double)period;
		t					= &(x->x_beats[period]);
		x->x_beatexpect		= now + (double)t->expect;
		x->x_on_beat	    = t->onbeat;
		
time:
		x->x_lastlasttime = x->x_lasttime;
		x->x_lasttime = x->x_this_input; //now;
		x->x_last_input = now;

		if(x->x_on_beat)outlet_bang(x->x_outbeat);
		outvalue = (t_float)beat_evaluate(x);
		outlet_float(x->x_outms, outvalue);
		if(x->x_beat_period)outlet_float(x->x_outbpm, (t_float)(60000.0 / outvalue));
	}
	return;
}

static void beat_ft1(t_beat *x, t_floatarg f)
{
	x->x_velo = (t_int)f;
}

	/* toggle printing on/off */
static void beat_print(t_beat *x)
{
	if(x->x_print)x->x_print = 0;
	else x->x_print = 1;
}

static void beat_reset(t_beat *x)
{
	int i;

	for(i = 0; i < BEAT_LONG; i++)
	{
		x->x_beats[i].points = 0;
		x->x_beats[i].expect = i;
		x->x_beats[i].onbeat = 0;
	}
	x->x_lastlasttime = 0;
	x->x_lasttime = 0;
	x->x_num_beats = 0;
	x->x_beat_period = 0;
	x->x_on_beat = 0;
}

static t_class *beat_class;

static void beat_free(t_beat *x)
{
	/* nothing to do */
}

static void *beat_new(t_floatarg f)
{
    t_beat *x = (t_beat *)pd_new(beat_class);
    inlet_new(&x->x_ob, &x->x_ob.ob_pd, gensym("float"), gensym("ft1"));
	x->x_outbpm = outlet_new(&x->x_ob, gensym("float"));
	x->x_outms = outlet_new(&x->x_ob, gensym("float"));
	x->x_outbeat = outlet_new(&x->x_ob, gensym("bang"));

	beat_reset(x);
	x->x_band_percent = 4;	/* allow 4% 'jitter' by default */
	if(f)x->x_band_percent = (t_int)f;

	post("beat: band percentage set to %d", x->x_band_percent);

    return (void *)x;
}

#ifndef MAXLIB
void beat_setup(void)
{
    beat_class = class_new(gensym("beat"), (t_newmethod)beat_new,
    	(t_method)beat_free, sizeof(t_beat), 0, A_DEFFLOAT, 0);
#else
void maxlib_beat_setup(void)
{
    beat_class = class_new(gensym("maxlib_beat"), (t_newmethod)beat_new,
    	(t_method)beat_free, sizeof(t_beat), 0, A_DEFFLOAT, 0);
#endif
    class_addfloat(beat_class, beat_float);
	class_addmethod(beat_class, (t_method)beat_ft1, gensym("ft1"), A_FLOAT, 0);
	class_addmethod(beat_class, (t_method)beat_reset, gensym("reset"), 0);
	class_addmethod(beat_class, (t_method)beat_print, gensym("print"), 0);
#ifndef MAXLIB
    
    logpost(NULL, 4, version);
#else
	class_addcreator((t_newmethod)beat_new, gensym("beat"), A_DEFFLOAT, 0);
    class_sethelpsymbol(beat_class, gensym("maxlib/beat-help.pd"));
#endif
}