/*
 *  play.c - Plays module
 *
 *  (C) 1994 Mikael Nordqvist (d91mn@efd.lth.se, mech@df.lth.se)
 */

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/soundcard.h>
#include <sys/ultrasound.h>
#include <limits.h>
#include <sys/time.h>

#include "mod.h"
#include "message.h"

/* Sample #0 is used as a "silent sample" that will cause the active voice
 * to be silenced.
 */

SEQ_DECLAREBUF();
extern int seqfd, gus_dev;
extern struct mod_info M;
extern struct options opt;

/* Variables used to play module */

struct voice V[MAX_VOICES];
struct effects efx;
struct event *cur_event;

int songpos, linepos, tick, speed, tempo;
double mod_time, tick_time;
char is_first_ult_effect;

char song_end, restart_song, advance_songpos;

/* Used to make sure we notify main of speed/tempp-changes */
static int last_speed, last_tempo;

/* Tables */
extern int periodtable[NR_OCTAVES*12];

void play_tick(void)
{
    int i;

    tick++;
    if(tick >= speed) { /* Next line/position ... */
	tick=0;

	if(efx.pattern_delay)   /* Update patterndelay if needed */
	    --efx.pattern_delay;
	
	/* Check line jumps (EFX_LOOP) on tick #0 in each line */
	    
	if(efx.PBreakFlag) {
	    linepos=efx.PBreakPos;
	    efx.PBreakPos=0;
	    efx.PBreakFlag=0;
	    advance_songpos=0;
	}
	
	/* Check patternjumps (EFX_BREAK|JMP) on tick #0 in each line. */
	
	/* NOTE: A line with EFX_LOOP and then EFX_BREAK on a voice with
	 * higher number will change pattern and start on line 0 regardless
	 * of the argument to EFX_BREAK. This seems like a bug, but
	 * as this is the behaviour of ProTracker, we will emulate it.
	 */
	    
	if(efx.PosJumpFlag) {
	    linepos=efx.PBreakPos;  /* Set line to continue at */
	    advance_songpos=1;
	}
	else {
	    if(!efx.pattern_delay) {
		if(++linepos == 64) { /* Just advance normally */
		    linepos=0;
		    advance_songpos=1;
		}
	    }
	}

	if(advance_songpos) {
	    songpos++;
	    if(M.format == MODFORMAT_MOD || M.format == MODFORMAT_MTM)
		songpos=(songpos&0x7f); /* PT/MTM allows 0x80 songpos's */
	    if(M.format == MODFORMAT_ULT)
		songpos=(songpos&0xff); /* ULT allows 0x100 songpos's */
	    if(M.format == MODFORMAT_S3M) {
		while(songpos < M.songlength) {
		    if(M.patterntable[songpos] == S3M_ORDER_SKIP) {
			songpos++;
			continue;
		    }
		    if(M.patterntable[songpos] == S3M_ORDER_SONGEND)
			songpos=M.songlength;
		    break;
		}
	    }

	    /* End song if it's done or an invalid pattern # is found */
	    if(songpos >= M.songlength ||
	       M.patterntable[songpos] > M.nr_patterns) {
		restart_song=1;
		if(opt.loop_module || (!opt.break_loops && M.restartpos))
		    songpos=M.restartpos;
		else
		    song_end=1;
	    }
	    efx.PBreakPos=0;
	    efx.PosJumpFlag=0;
	    restart_song=0;     /* Either set above or in EFX_JUMP */
	}
    }
    
    if(song_end) {
	for(i=0; i < M.nr_voices; ++i)
	    MY_SEQ_STOP_NOTE(gus_dev, i, V[i].note, 0);
	
	SEQ_WAIT_TIME((int)mod_time+10); /* Wait 100 extra milli-seconds */
	SEQ_WAIT_TIME((int)mod_time+11); /* Dummy to make sure we don't exit
					  * until above events has taken effect
					  */
	SEQ_ECHO_BACK(MESSAGE_DONE);
	return; /* No printing or processing when song is done */
    }
    
    if(!tick && !efx.pattern_delay) {
	if(advance_songpos)
	    advance_songpos=0;
	
	/* ALWAYS send speed _before_ line (catchup-code depends on this! */
	if(speed != last_speed || tempo != last_tempo) {
	    send_speed();
	    last_speed=speed;
	    last_tempo=tempo;
	}
	send_line();
    }
    process_line(efx.pattern_delay ? 1 : 0);
    
    mod_time+=tick_time;
    SEQ_WAIT_TIME((int)(mod_time+0.5));
}


/* When continue_effects is nonzero we just continue uppdating effects that
 * are updated when tick is nonzero (this is for EFX_PATTERNDELAY).
 */

void process_line(int continue_effects)
{
    int v;
    struct event tmp_event, *orig_event;
    int this_songpos=songpos; /* In case songpos gets changes in EFX_? */

    for(v=0; v < M.nr_voices; ++v) {
	efx.set_volume=0;
	efx.set_finepitch=0;
	efx.set_balance=0;
	efx.dont_trigger_note=0;
	efx.retrig_note=0;
	efx.kill_voice=0;

	cur_event=GET_EVENT_PTR(v, M.patterntable[this_songpos], linepos);
	
	if(!tick && !continue_effects) { /* Tick #0 */
	    
	    /* Sample change on the voice? */
	    if(cur_event->sample) {
		if(cur_event->sample <= M.nr_samples &&
		   M.sample[cur_event->sample].valid) {
		    if(cur_event->sample != V[v].sample) {
			/* See TECH-file for a comment on changing samples
			 */
			SEQ_SET_PATCH (gus_dev, v, cur_event->sample);
			V[v].sample=cur_event->sample;
		    }
		    
		    /* Try to reduce number of calls to do_set_finepitch() */
		    if(V[v].finetune != M.sample[cur_event->sample].finetune
		       || V[v].pitchbend)
			efx.set_finepitch=1;
		    
		    V[v].finetune=M.sample[cur_event->sample].finetune;
		    V[v].sampleoffset=0;
		}
		else {
		    /* Change to nonspecified sample -> silence the voice */
		    efx.kill_voice=1;
		}
	    }
	    /* Reset volume to default instrument-volume if we have
	     * a (valid) sample specified. Also make sure it affects
	     * a sample that is already playing.
	     */
	    if(cur_event->sample && M.sample[cur_event->sample].valid) {
		V[v].volume=V[v].real_volume=M.sample[V[v].sample].volume;
		efx.set_volume=1;
	    }
	    
	    /* Second effect is used for "volume"-field in S3M's */
	    if(M.format == MODFORMAT_S3M && cur_event->effect2 == EFX_VOLUME) {
		V[v].volume=V[v].real_volume=cur_event->arg2;
		efx.set_volume=1;
	    }
	    
	    is_first_ult_effect=1;
	    check_tick0_efx(v); /* Check effects */

	    /* Check second effect if its a .ULT-file.
	     * This is 'hack' but it does the job w/o requiring modifications
	     * to effects.c.
	     */
	    if(M.format == MODFORMAT_ULT) {
		is_first_ult_effect=0;
		tmp_event=*cur_event;
		orig_event=cur_event;
		cur_event=&tmp_event;
		cur_event->effect=cur_event->effect2;
		cur_event->arg=cur_event->arg2;
		
		check_tick0_efx(v);
		cur_event=orig_event;
	    }

	    /* Silence voice if needed */
	    if(efx.kill_voice) {
		V[v].sample=0;
		V[v].volume=V[v].real_volume=0;
		MY_SEQ_START_NOTE(gus_dev, v, 255, 0);
	    }

	    /* Set balance if needed */
	    if(efx.set_balance)
		SEQ_PANNING(gus_dev, v, ((int)M.panning[v])*17-128);

	    /* If we have a note and no EFX_PORTANOTE(&VS) or EFX_NOTEDELAY,
	     * we trigger it (unless it's a NOTE_OFF ofcourse)
	     */
	    if(cur_event->note && cur_event->note != NOTE_OFF &&
	       !efx.dont_trigger_note) {
		if(V[v].sample) {
		    MY_SEQ_START_NOTE(gus_dev, v, cur_event->note,
				      convert_volume(V[v].real_volume));
		    if(V[v].sampleoffset)
			GUS_VOICE_POS(gus_dev, v, V[v].sampleoffset);
		    if(efx.set_finepitch)
			do_set_finepitch(v);
		}
	    }
	    else { /* No note to play, change volume/finepitch if needed
		    * (and there is a sample specified for the voice).
		    */
		if(cur_event->note == NOTE_OFF)
		    MY_SEQ_STOP_NOTE(gus_dev, v, 0, 0);

		if(V[v].sample) {
		    if(efx.set_volume)
			MY_SEQ_START_NOTE(gus_dev, v, 255,
				       convert_volume(V[v].real_volume));
		    if(efx.set_finepitch)
			do_set_finepitch(v);
		}
	    }
	}
	else {  /* NOT tick #0 */
	    is_first_ult_effect=1;
	    check_efx(v); /* Update effects */
	    
	    /* Update second effect if its a .ULT-file.
	     */
	    if(M.format == MODFORMAT_ULT) {
		is_first_ult_effect=0;
		tmp_event=*cur_event;
		orig_event=cur_event;
		cur_event=&tmp_event;
		cur_event->effect=cur_event->effect2;
		cur_event->arg=cur_event->arg2;
		
		check_efx(v);
		cur_event=orig_event;
	    }
	    
	    if(V[v].sample) {
		if((efx.retrig_note && cur_event->note != NOTE_OFF) ||
		   efx.set_volume)
		    MY_SEQ_START_NOTE(gus_dev, v,
				   (efx.retrig_note ? cur_event->note : 255),
				   convert_volume(V[v].real_volume));
		
		if((efx.retrig_note && cur_event->note != NOTE_OFF) &&
		   V[v].sampleoffset)
		    GUS_VOICE_POS(gus_dev, v, V[v].sampleoffset);
		
		/* Here we always set finepitch if we have efx.retrig_note
		 * as the efx.set_finepitch that is set at tick #0 is already
		 * cleared, and there could be a finetunedifference.
		 */
		if(efx.retrig_note || efx.set_finepitch)
		    do_set_finepitch(v);
	    }
	}
    }
}


void total_reset(int spd, int tmpo, int pos)
{
    init_sequencer_voices(); /* Must be done first */
    init_voices();

    last_speed=last_tempo=-1;
    speed=spd;
    tempo=tmpo;
    tick_time=250.0/tempo;

    SEQ_START_TIMER();
    mod_time=0;
    SEQ_WAIT_TIME(0);
    SEQ_DUMPBUF();
    tick=speed;

    set_modulepos(pos);
}


void init_sequencer_voices(void)
{
    int i;
    
    ioctl(seqfd, SNDCTL_SEQ_RESET, 0);   /* Kill events */
    _seqbufptr=0;
    GUS_NUMVOICES(gus_dev, M.nr_voices);
    SEQ_VOLUME_MODE(gus_dev, VOL_METHOD_LINEAR);

    for(i=0; i < M.nr_voices; ++i) {
	SEQ_PANNING(gus_dev, i, ((int)M.panning[i])*17-128);
	SEQ_BENDER_RANGE(gus_dev, i, 8191);
	SEQ_PITCHBEND(gus_dev, i, 0);
    }
}


void init_voices(void)
{
    int i;

    for(i=0; i < M.nr_voices; ++i) {
	/* Clear voice-info */
	bzero((void*)&V[i], sizeof(struct voice)); 
	
	/* Waveforms are retriggered sines by default */
	V[i].vibrato_waveform=WAVEFORM_SINE;
	V[i].vibrato_retrig=1;
	V[i].tremolo_waveform=WAVEFORM_SINE;
	V[i].tremolo_retrig=1;
	V[i].loopstartpos=-1;

	/* Set note and period to valid values so pitchbender won't screw up */
	V[i].note=BASE_NOTE+4*12;
	V[i].period=V[i].real_period=periodtable[V[i].note-BASE_NOTE];
    }
}


void set_modulepos(int pos)
{
    efx.pattern_delay=0;
    
    efx.PosJumpFlag=0;
    efx.PBreakFlag=0;
    efx.PBreakPos=0;
    
    songpos=pos-1;
    linepos=63;
    song_end=0;
    restart_song=0;
}


/* Converts 'v' to a volumelevel suitable for the sounddriver */

int convert_volume(int v)
{
    return MIN((v*128)/M.volrange, 127);
}


/* Sets the pitch for a voice. Should actually be in effects.c, but I wanted
 * to keep it clean from sequencer-commands.
 */

void do_set_finepitch(int v)
{
    set_pitch(v);
    
    SEQ_PITCHBEND(gus_dev, v, V[v].pitchbend+(V[v].finetune*100)/8);
#if 0
    if(opt.active_voices&(1<<v) && opt.verbose >=5)
	printf("'%d+%d",V[v].pitchbend, V[v].finetune);
#endif
}
