/*
	Hacked guts out of:
	An ALSA MIDI mapper
	(C) 2002-2005 David Given, http://www.cowlark.com/amidimap/

	by Thomas Orgis in 2010, licensed under GPLv2

	This is a hacky proof of concept for getting my M-Audio XSession pro to excert some effect on mpg123, on Linux systems with ALSA sequencer API.

	The idea is to run two instances of mpg123 like that:

		mpg123 -R --fifo /dev/shm/mpg123-1
		mpg123 -R --fifo /dev/shm/mpg123-2

	Then run this program here with a playlist (listing of absolute file paths) from STDIN

		cat absolute-playlist | ./mpg123-control /dev/shm/mpg123-1 /dev/shm/mpg123-2

	and connect it to the MIDI controller (adjust port numbers):

		aconnect 20:0 129:0

	With that setup, you will load tracks from the playlist with the eject buttons for the respective channel, start/stop playback with the play/pause buttons... the volume and pitch faders will do something you might expect... the EQ buttons change the EQ settings.
	In short: You have a nifty basic DJ setup on your PC with two plain instances of mpg123.

	Note that the pitching only works in a range the hardware or your audio output support (i.e. ALSA dmix)... and you need to be able to open two audio outputs at once, at different rates (ALSA dmix can do that... but it rather stresses the CPU).
	Anyhow, it is just a fun proof-of-concept to be able to get some reaction out of my MIDI toy.

	For building this code, you just need to link with libasound:
		cc -o mpg123-control mpg123-control.c -lasound
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <sys/poll.h>
#include <alsa/asoundlib.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <math.h>

/* Everything arrives at channel 0 ... */
/* The keys, they give note on/off. */
enum con_note
{
	 PREV_A=46
	,NEXT_A=43
	,PLAY_A=70
	,EJECT_A=58
	,CUE_A=44

	,PREV_B=56
	,NEXT_B=57
	,PLAY_B=69
	,EJECT_B=59
	,CUE_B=45
};

enum button
{
	 PREV=0
	,NEXT
	,PLAY
	,EJECT
	,CUE
};

/* The controllers ... first the faders: */
enum con_cc
{
	/* The fader is mapped to two diametral controllers.
	   For realistic fader action, one might want to set volume of channel A to 100% for fader A >= 60, to 0 when fader A == 0 . */
	 FADE_A=20
	,PITCH_A=12
	,VOL_A=11
	,LOW_A=29
	,MID_A=28
	,HIGH_A=27
	,MISC_1_A=24
	,MISC_2_A=25
	,MISC_3_A=26

	,FADE_B=17
	,PITCH_B=15
	,VOL_B=14
	,LOW_B=36
	,MID_B=35
	,HIGH_B=34
	,MISC_1_B=31
	,MISC_2_B=32
	,MISC_3_B=33
};

enum control
{
	 FADE=0
	,PITCH
	,VOL
	,LOW
	,MID
	,HIGH
	,MISC_1
	,MISC_2
	,MISC_3
};

int inport = 0;
int outport = 0;

/* output descriptor and fader/volume value per channel */
FILE* chanfile[2];
int chanfade[2];
int chanvol[2];
double chaneq[2][3];

char* linebuf = NULL;
size_t linesize = 0;

/* The handle to the ALSA sequencer. */

snd_seq_t* sequencer = NULL;

/* ======================================================================= */
/*                                UTILITIES                                */
/* ======================================================================= */

void error(char* format, ...)
{
	va_list ap;
	va_start(ap, format);
	fprintf(stderr, "Error: ");
	vfprintf(stderr, format, ap);
	fprintf(stderr, "\n");
	va_end(ap);

	exit(1);
}

void printconditional(char* prefix, int value)
{
	if (value >= 0)
		printf("%s%d ", prefix, value);
}

/* Read a line from a FILE, automatically allocating storage. */
size_t my_getline(FILE *in, char **out, size_t *size)
{
	size_t fill = 0; /* length of the string, without \0 */
	if(*size == 0 || *out==NULL)
	{
		*size = 10;
		*out = (char*)malloc(*size); /* Increase that later. */
		if(*out == NULL)
		{
			error("D'OOM!");
			return 0;
		}
	}
	while(1)
	{
		if(fgets(*out+fill, *size-fill, in) == NULL)
		return fill;

		fill = strlen(*out);

		/* Check if we really got a line or need to increase the buffer and continue.
		   The last byte is always \0, before that we want a \n or \r. */
		if((*out)[fill-1] == '\n' || (*out)[fill-1] == '\r') // a half \r\n sequence could be there...
		return fill;

		/* Else, realloc to get more data in. */
		if(*size > ((unsigned int)-1)/4)
		{
			error("Maximum line length reached!");
			return 0;
		}
		char *tmp = (char*)realloc(*out, 2 * (*size));
		if(tmp == NULL){	error("D'OOM!"); return 0; }

		*out = tmp;
		*size *= 2;
	}
}

/* Cut off the line end... */
void chomp(char *line, size_t length)
{
	if(length == 0) length = strlen(line);

	if(length < 1) return;

	size_t pos = length-1;
	/* Handle the end zero byte being included in the length.
	   Skip it, including detection of empty string. */
	if(line[pos] == 0)
	{
		if(pos > 0) --pos;
		else return;
	}

	/* Now chop off any line end markers until a non-marker is found, or we are at the beginnig of the line. */
	do
	{
		if(line[pos] == '\n' || line[pos] == '\r') line[pos] = 0;
		else return;

		if(pos > 0) --pos;
	} while(pos > 0);
}

/* ======================================================================= */
/*                                 ALSA                                    */
/* ======================================================================= */

void open_alsa_connection(void)
{
	int client;
	
	if (snd_seq_open(&sequencer, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0)
		error("Couldn't open ALSA sequencer");
		
	snd_seq_set_client_name(sequencer, "mpg123-control");
	client = snd_seq_client_id(sequencer);

	inport = snd_seq_create_simple_port(sequencer, "mpg123-control IN",
		SND_SEQ_PORT_CAP_WRITE|SND_SEQ_PORT_CAP_SUBS_WRITE,
		SND_SEQ_PORT_TYPE_APPLICATION);
	if (inport < 0)
		error("Couldn't create input port");

	outport = snd_seq_create_simple_port(sequencer, "mpg123-control THRU",
		SND_SEQ_PORT_CAP_READ|SND_SEQ_PORT_CAP_SUBS_READ,
		SND_SEQ_PORT_TYPE_APPLICATION);
	if (outport < 0)
		error("Couldn't create output port");

	printf("IN = %d:%d, OUT = %d:%d\n",
		client, inport,
		client, outport);
}

/* ======================================================================= */
/*                 Controller / Button hanlding                            */
/* ======================================================================= */


int map_controller(int num, int* rchan, enum control *rcon)
{
	int chan;
	enum control con;
	switch(num)
	{
		case FADE_A: chan=0; con=FADE; break;
		case PITCH_A: chan=0; con=PITCH; break;
		case VOL_A: chan=0; con=VOL; break;
		case LOW_A: chan=0; con=LOW; break;
		case MID_A: chan=0; con=MID; break;
		case HIGH_A: chan=0; con=HIGH; break;
		case MISC_1_A: chan=0; con=MISC_1; break;
		case MISC_2_A: chan=0; con=MISC_2; break;
		case MISC_3_A: chan=0; con=MISC_3; break;

		case FADE_B: chan=1; con=FADE; break;
		case PITCH_B: chan=1; con=PITCH; break;
		case VOL_B: chan=1; con=VOL; break;
		case LOW_B: chan=1; con=LOW; break;
		case MID_B: chan=1; con=MID; break;
		case HIGH_B: chan=1; con=HIGH; break;
		case MISC_1_B: chan=1; con=MISC_1; break;
		case MISC_2_B: chan=1; con=MISC_2; break;
		case MISC_3_B: chan=1; con=MISC_3; break;

		default:
			fprintf(stderr, "Unknown controller!\n");
			return -1;
	}
	*rchan = chan;
	*rcon = con;
	return 0;
}

/* Scale 0..127 to 0..1 */
double scale(int ival)
{
	if(ival == 0) return 0.;

	if(ival == 127) return 1.;

	return (double)ival/127;
}

/* Map 0..127 to -1..0..1, with some range around 0. */
double midscale(int ival)
{
	if(ival > 61 && ival < 66)
	return 0.;

	if(ival == 0) return -1.;

	if(ival == 127) return 1.;

	if(ival <= 61) return (double)(ival-62)/62;

	if(ival >= 66) return (double)(ival-65)/62;
}

/* Map a fader value to volume factor.
   above 60, map to 1.  */
double fadescale(int ival)
{
	if(ival >= 61) return 1.;

	return (double)ival/61;
}

void set_vol(int chan)
{
	double vol = scale(chanvol[chan]) * fadescale(chanfade[chan]) * 100.;

	fprintf(chanfile[chan], "v %g\n", vol);
}

void set_eq(int chan, int band, int ival)
{
	double dbval;
	double eqval;
	double preval = midscale(ival);
	/* Take the value in [-1;1], scale to [-26;0] and [0;6] dB */

	if(preval < 0.)
	{
		dbval = preval * 26.;
	}
	else if(preval > 0.)
	{
		dbval = preval * 6.;
	}

	chaneq[chan][band] = pow(10., dbval/20.);
	fprintf(chanfile[chan], "seq %g %g %g\n", chaneq[chan][0], chaneq[chan][1], chaneq[chan][2]);
}

void set_pitch(int chan, int ival)
{
	double val = midscale(ival); /* Quite a pitch range... full stop to double speed. */
	fprintf(chanfile[chan], "pitch %g\n", val);
}

void handle_controller(int num, int val)
{
	int chan;
	enum control con;
	if(map_controller(num, &chan, &con) != 0)
	return;

	switch(con)
	{
		case FADE:
			chanfade[chan] = val;
			set_vol(chan);
		break;
		case VOL:
			chanvol[chan] = val;
			set_vol(chan);
		break;
		case LOW:
			set_eq(chan, 0, val);
		break;
		case MID:
			set_eq(chan, 1, val);
		break;
		case HIGH:
			set_eq(chan, 2, val);
		break;
		case PITCH:
			set_pitch(chan, val);
		break;

		/* Nothing more yet... not handling all controls. */
	}

	fflush(chanfile[chan]);
}

int map_button(int num, int* rchan, enum button *rbutt)
{
	int chan;
	enum button butt;
	switch(num)
	{
		case PREV_A: chan=0; butt=PREV; break;
		case NEXT_A: chan=0; butt=NEXT; break;
		case PLAY_A: chan=0; butt=PLAY; break;
		case EJECT_A: chan=0; butt=EJECT; break;
		case CUE_A: chan=0; butt=CUE; break;

		case PREV_B: chan=1; butt=PREV; break;
		case NEXT_B: chan=1; butt=NEXT; break;
		case PLAY_B: chan=1; butt=PLAY; break;
		case EJECT_B: chan=1; butt=EJECT; break;
		case CUE_B: chan=1; butt=CUE; break;

		default:
			fprintf(stderr, "Bad button!\n");
			return -1;
	}
	*rchan = chan;
	*rbutt = butt;
	return 0;
}

void handle_button(int num)
{
	int chan;
	enum button butt;

	if(map_button(num, &chan, &butt) != 0)
	return;

	switch(butt)
	{
		case PREV: fprintf(chanfile[chan], "jump -10s\n"); break;
		case NEXT: fprintf(chanfile[chan], "jump +10s\n"); break;
		case PLAY: fprintf(chanfile[chan], "p\n"); break;
		case EJECT:
		{
			size_t ll;
			/* Try to read the next track from standard input. */
			fprintf(stderr, "Trying to get next track...\n");
			ll = my_getline(stdin, &linebuf, &linesize);
			if(ll > 0)
			{
				chomp(linebuf, ll);
				fprintf(stderr, "Got: %s\n", linebuf);
				fprintf(chanfile[chan], "lp %s\n", linebuf);
			}
			else
			{
				fprintf(stderr, "Failed! Nothing there?\n");
			}
		}
		break;
	}
	fflush(chanfile[chan]);
}

void process_events(void)
{
	int numpolldesc = snd_seq_poll_descriptors_count(sequencer, POLLIN);
	struct pollfd* pfd;
	snd_seq_event_t* event;
	int i;

	pfd = malloc(sizeof(struct pollfd) * numpolldesc);
	if (!pfd)
		error("Memory allocation failed");
	
	snd_seq_poll_descriptors(sequencer, pfd, numpolldesc, POLLIN);
	
	for (;;)
	{
		int oldnote = -1;
		int addnote = -1;
		if (snd_seq_event_input(sequencer, &event) < 0)
		{
			poll(pfd, 1, 100000);
			continue;
		}

		snd_seq_ev_set_subs(event);  
		snd_seq_ev_set_direct(event);

		switch (event->type)
		{
			case SND_SEQ_EVENT_CONTROLLER:
				handle_controller(event->data.control.param, event->data.control.value);
			break;
			case SND_SEQ_EVENT_NOTEON:
			/* case SND_SEQ_EVENT_NOTEOFF: */
				/* For some reason, I get NOTEON on note off, too... using velocity to tell that apart. */
				if(event->data.note.velocity > 0)
				handle_button(event->data.note.note);
			break;
		}

		/* Pass the event through, just for fun. */
		snd_seq_ev_set_source(event, outport);
		i = snd_seq_event_output_direct(sequencer, event);
		if (i < 0)
			error("snd_seq_event_output_direct() failed: %s", snd_strerror(i));

		snd_seq_free_event(event);
	}
}

/* ======================================================================= */
/*                              MAIN PROGRAM                               */
/* ======================================================================= */


int main(int argc, char* argv[])
{
	int i;
	for(i=0;i<sizeof(chanfile)/sizeof(FILE*); ++i)
	{
		if(argc <= i+1)
		{
			fprintf(stderr, "Need more arguments.\n");
			return 1;
		}
		fprintf(stderr, "Opening %s for channel %i\n", argv[i+1], i);
		chanfile[i] = fopen(argv[i+1], "w");
		if(chanfile[i] == NULL)
		{
			fprintf(stderr, "Cannot open %s!", argv[i+1]);
			return 2;
		}
		chanfade[i] = 127;
		chanvol[i] = 127;
		chaneq[i][0] = 1.;
		chaneq[i][1] = 1.;
		chaneq[i][2] = 1.;
	}

	open_alsa_connection();
	process_events();

	for(i=0;i<sizeof(chanfile)/sizeof(FILE*); ++i)
	fclose(chanfile[i]);

	if(linebuf != NULL) free(linebuf);

	return 0;
}


