// Play, a sound player Object by J. Laroche. June 1992
// Version 2.0

#import "Play.h"
#import<libc.h>

#define Error(A,B) if((A)) { if (delegate  && [delegate \
	respondsTo:@selector(error:)]) \
	{ strcpy(errorMessage,B); \
	[delegate error:errorMessage];} \
	fprintf(stderr,"%s\n", B); \
	SNDRelease(SND_ACCESS_OUT|SND_ACCESS_DSP,dev_port,owner_port); \
	return -1;} 

//#define Error(A,B) if((A)) { printf("%s \n",B); return self;}

#define DMASIZE 2048
#define MEMMAX 4000
#define WRITE_TAG 0

#define READ_BUF_SIZE	(vm_page_size / BYTES_PER_16BIT)


static void HandleDSPMessage(msg_header_t *msg, void *userData)
{
    [(Play *)userData playCompleted:0];
}

static void PlayMonitor(DPSTimedEntry tag, double now, char *userData)
{
    [(Play *)userData playMonitor];
}


@implementation Play:Object


//------------	 Create a new Play object.

+ new
{
    self = [super new];
    
    stereo = 0;
    verbose = 1;
    CHANNEL = MONO;
    AES = 0;
    INIT = 0;
    ster = 0;
    U = 0; D = 0; K = 0; Filter_length = 0;
    S = 0;
    low_water = 48*1024;
    high_water = 512*1024;
    thresh = 0.003;
    offset=4;
    max_order = (int) (MEMMAX / 150);
    status = IDLE;
    S_rateIn = 16000;
    errorMessage = calloc(1024,sizeof(char));
    WAIT = NO;
    gain = 0.9;
    return self;
}

- setDelegate:anObject
{
	delegate = anObject;
	
	return self;
}


- setSamplingRate:(int)aSamplingRate
{
    S_rateIn = aSamplingRate;
    return self;
}


////////// - setOutput:(int)anOutput. Sets output to desired output:
//// AES_HIGH : ssi port, to A/D64X converters, 48kHz, digital.
//// AES_LOW : ssi port, to A/D64X converters, 44.1kHz, digital.
//// 0 : NeXT Dacs.


- setOutput:(int)anOutput
{
    AES = anOutput;
    return self;
}

////////////////////////// - setMonoMode:(int)aMode. Sets sound playing mode.
///// MONO or STEREO.

- setMonoMode:(int)aMode
{
    CHANNEL = aMode;
    stereo = (CHANNEL != MONO);
    return self;
}

////////////// - playSound:(SNDSoundStruct*)aSound:(int)begin:(int)length
///// Starts playing length samples of sound, starting from begin.
///// When finished, sends the delegate a didPlay message.

- (int) playSound:(SNDSoundStruct*)aSound:(int)begin:(int)length
{
int i,j;
static int AESStat = 0;
short *beginSound, *endSound;
int beginPage, endPage, pageNumber;
//Sound *DSPprogram;

    
    if(status != IDLE) return -1;
    if(aSound == 0) Error(1,"The sound file is null!");

    length = MIN(length*(1+stereo),aSound->dataSize/sizeof(short) - begin);
    if(aSound->dataSize/sizeof(short) < begin)
	Error(1,"Sound not long enough for selected duration");

    first_samp = begin;
    size_samp = length;
    if(!AES) INIT = 0;
    else if(AES != AESStat) {INIT = 1; AESStat = AES;}
    else INIT = 0;
    
    k_err = SNDAcquire(SND_ACCESS_OUT|SND_ACCESS_DSP,0,0,0,
    	NULL_NEGOTIATION_FUN,0,&dev_port,&owner_port); 
    Error(k_err,"DSP or DACs Busy, or access to devices denied\n Are you using another machine's DSP?");
    k_err = SNDReset(SND_ACCESS_OUT|SND_ACCESS_DSP,dev_port,owner_port); 
    Error(k_err,"Error during initialization.");
    
    k_err = snddriver_get_dsp_cmd_port(dev_port,owner_port,&cmd_port);

	// Here's an important setting. It appears that this size make the 
	// whole thing work. others don't. I don't know why, but it has
	// something to do with the ramp created by the snd driver.
    k_err =  snddriver_set_sndout_bufsize(dev_port,owner_port,512);
    k_err =  snddriver_set_sndout_bufcount(dev_port,owner_port,2);
    Error(k_err,"Error during initialization.");
    
    
    // From now on, we consider we're playing, even if it's not true.

/////////////// Calculate Sampling rate conversion param. //////////////////
    
    S = ((AES == AES_LOW || AES == 0)? 44100 : 48000);
    [self calculateRatio:(float)S_rateIn/(float)S:rat:thresh];
    D = rat[0];
    U = rat[1];
    if(D == 0 || U == 0) Error(1,"Weird sampling rate, can't play sound.");


///////////// Examine sound file, set stereo conversion param. ///////////////

    
    if(verbose)
    {
    printf("Playing\t\t%s\n",file[0]);
    if(AES) printf("Output:\t\tA/D64X AES/EBU, %skHz\n",
    	((AES == AES_LOW) ? "44.1" : "48"));
    else printf("Output:\t\t NeXT D/A converters\n");
   
    printf("Sampling rate:\t%d\n", S_rateIn);
    switch(CHANNEL)
    {
	case STEREO : printf("Mode: \t\tStereo\n"); break ;
	case MONO : printf("Mode:\t\tMono\n"); break ;
    }
    }


/////////////// Calculate FIR Filter characteristics //////////////////
    
    if(U == 1 && D == 1) K = 3;
    else
//	if(AES == 0) K = MIN((int)(40*(2-stereo)*U/D), MIN(MEMMAX/U,60));
//	else K = MIN((int)(20*(2-stereo)*U/D), MIN(MEMMAX/U,40));
	K = MIN((int)(55*(2-stereo)),MEMMAX/U);
    K = MIN(K,250);
    if(verbose) 
    printf("Filter Length: %d,  Up-factor %d,  Down-factor %d\n",K,U,D);
    
    Filter_length = (((K*U) % 2)? K*U : K*U+1);

    para[0] = U;
    para[1] = D;
    para[2] = K-1;
    para[3] = Filter_length;
 
 
    
/////////////// Initialize sound and dsp driver //////////////////
    
        
    protocol = SNDDRIVER_DSP_PROTO_RAW;
    k_err = snddriver_stream_setup(dev_port, owner_port,
			SNDDRIVER_DMA_STREAM_TO_DSP,
			DMASIZE, 2, 
			low_water, high_water,
			&protocol, &write_port);
    Error(k_err,"Error during Stream Set-up");
    k_err = snddriver_stream_setup(dev_port, owner_port,
			SNDDRIVER_STREAM_DSP_TO_SNDOUT_44,
			DMASIZE, 2, 
			low_water, high_water,
			&protocol, &read_port);
    Error(k_err,"Error during Stream Set-up");

    k_err = snddriver_dsp_protocol(dev_port, owner_port, protocol);   
    k_err = port_allocate(task_self(),&reply_port);



/////////////// Get DSP program and boot DSP. //////////////////
    
    dspStruct = (SNDSoundStruct *)getsectdata("__SND", "play.snd",&i);
    Error(!dspStruct,"Cannot find DSP program!\nSomething's wrong");
//    k_err = SNDReadDSPfile("play.lod", &dspStruct,(char*)&i);
//    Error(k_err,"Cannot find DSP program! Something's wrong");
    k_err = SNDBootDSP(dev_port, owner_port, dspStruct);
    Error(k_err,"Cannot boot DSP! Something's wrong");




/////////////// Send stereo param and filter coeff to DSP. //////////////////
    
    ster = stereo + 2 + 4*((AES != 0)) + 8*((AES == AES_HIGH)) + 
    	16*(INIT);
    k_err = snddriver_dsp_write(cmd_port,&ster,1,sizeof(int),
   						SNDDRIVER_LOW_PRIORITY);
    
    filtre = (int*) calloc(Filter_length,sizeof(int));
    if(U == 1 && D == 1)
    {
	filtre[0] = filtre[2] = 0;
	filtre[1] = 8388607 * gain;
    }
    else
    {
	cut = 3.14159265 * (1/(float) MAX(U,D) - offset/K/U);
	if(cut <= 0) 
	{
	    printf("Problem with the filter's cut-off frequency because U or D are too large, \nyou might get aliasing\n");
	    cut = 3.14159265 * (1/(float) MAX(U,D));
	}
	[self calculateFilter:filtre:Filter_length:cut: U];
    }
    
    k_err = snddriver_dsp_write(cmd_port,para,4,sizeof(int),
   						SNDDRIVER_LOW_PRIORITY);
    k_err = snddriver_dsp_write(cmd_port,filtre, Filter_length,sizeof(int),
   						SNDDRIVER_LOW_PRIORITY);
/////////////// Allocate virtual memory for DMA_IN //////////////////
    

    vm_allocate(task_self(),(vm_address_t *)(&foo),vm_page_size,TRUE);
    Error(k_err,"VM Allocation Error ");
    vm_allocate(task_self(),(vm_address_t *)(&faa),4*DMASIZE,TRUE);
    Error(k_err,"VM Allocation Error ");
    
    beginSound = (short*)aSound + aSound->dataLocation/ sizeof(short) + begin;
    endSound = (short*)aSound+aSound->dataLocation/sizeof(short)+begin+length;
    
    
    beginPage = (((int)beginSound) % (vm_page_size)) / sizeof(short);
    endPage = (((int)endSound) % (vm_page_size)) / sizeof(short);
    
    
    pageNumber = (((int)endSound - (int)beginSound)/sizeof(short)
    	- endPage - (vm_page_size/2 - beginPage));

    if(pageNumber > 0) pageNumber /= (vm_page_size/2);
    else pageNumber = 0;
        
    if(INIT) usleep(1500000);
    
    
    for(i=0;i<length && beginPage<vm_page_size/2;i++, beginPage++)
	foo[beginPage] = *(beginSound++);
	
    if(i == length) pageNumber = -1;	// No need to send anything else.

    k_err = snddriver_stream_start_writing(write_port,(void *)foo,
	vm_page_size/2,WRITE_TAG,0,0,0,0,0,0,0,0, reply_port);

    if(pageNumber > 0)
    {
    k_err = snddriver_stream_start_writing(write_port,(void *)beginSound,
	pageNumber * vm_page_size/2,WRITE_TAG,0,0,0,0,0,0,0,0, reply_port);
    Error(k_err,"Error when starting playing");
    }

    if(pageNumber >= 0)
    {
    for(j=0, endSound -= endPage;j<endPage;j++)
	foo[j] = *(endSound++);
    for(;j<vm_page_size/2;j++)
	foo[j] = 0;
    
    k_err = snddriver_stream_start_writing(write_port,(void *)foo,
	vm_page_size/2,WRITE_TAG,0,0,0,0,0,0,0,0, reply_port);
    Error(k_err,"Error when starting playing");
    }
    
    k_err = snddriver_dsp_host_cmd(cmd_port,20,SNDDRIVER_LOW_PRIORITY);

    k_err = snddriver_stream_start_writing(write_port,(void *)faa,
	2*DMASIZE,WRITE_TAG,0,0,1,0,0,0,0,0, reply_port);
    Error(k_err,"Error when finishing playing");

    if(WAIT)
    {
    reply_msg = (msg_header_t *)malloc(MSG_SIZE_MAX);
    reply_msg->msg_size = MSG_SIZE_MAX;
    reply_msg->msg_local_port = reply_port;
    k_err = msg_receive(reply_msg, MSG_OPTION_NONE, 0);
    [self playCompleted:self];
    return 0;
    }
    
    DPSAddPort(reply_port,		
		HandleDSPMessage,		/* function to call */
		MSG_SIZE_MAX,	
		self,			/* first arg to HandleDSPMessage */
		NX_RUNMODALTHRESHOLD	/* priority */
		);
		
    play_mon = DPSAddTimedEntry(0.05, (DPSTimedEntryProc) PlayMonitor, self, NX_MODALRESPTHRESHOLD);
    status = PLAYING;
    return 0;
}




// Calculates FIR low-pass filter by windowing a sinc with a hamming window,
// and normalizing.

- calculateFilter:(int*)afiltre:(int)order:(float)freq_cut:(int)P
{
    int i;
    float scaler;
    float aux;

    for(i=1,scaler = 0;i<order/2;i++)
    {
        aux = (sin(freq_cut * i) / freq_cut / i 
	* (0.54 + 0.46 * cos(6.28318530*i/(order-2))));
	afiltre[order/2+i] = afiltre[order/2-i] = 8388607 * aux ;
	scaler += 2 * aux;
   }
    scaler = P * gain / (1 + scaler);
    afiltre[order/2] = 8388607 * scaler;
    for(i=1;i<order/2;i++)
        afiltre[order/2+i] = afiltre[order/2-i] = afiltre[order/2-i]*scaler;
    return self;
}


/*  calculateRatio returns in rat a ratio that best approximates the real
x with an relative error less than thresh, making sure U and D are less than
100. Thresh is equal to 0.03, the auditory threshold for pitch sensation.
*/ 

- calculateRatio:(float)x:(int*)arat:(float)athresh
{
int i=0,j;
float error = 1;
float reste;
int *A, *p, *q;
int prev_p = 1, prev_q = 1;

A = (int*) calloc(20,sizeof(int));
p = (int*) calloc(20,sizeof(int));
q = (int*) calloc(20,sizeof(int));

reste = x;

while(error/x > athresh)
{

    A[i] = floor(reste);
    reste = 1 / (reste - A[i]);
    for(j=i, p[i]=1, q[i]=A[i]; j>0; j--)
    {
	p[j-1] = q[j];
	q[j-1] = A[j-1] * q[j] + p[j];
    }
    error = fabs((float)q[0]/(float)p[0] - x);
    if(p[0] < max_order && q[0] < max_order)
    {
	prev_p = p[0];
	prev_q = q[0];
    }
    else
    {
    printf("Warning, difficult conversion: ideal factors U = %d, D = %d,\n",
	p[0],q[0]);
	p[0] = prev_p;
	q[0] = prev_q;
	error = fabs((float)q[0]/(float)p[0] - x);
	printf("Actual factors: U = %d, D = %d, \
Resulting Sampling rate error: %.2f %%\n",p[0],q[0],100*error/x);
	break;
    }
    i++;
}
arat[0] = q[0];
arat[1] = p[0];
return self;
}


        
- stop
{
    if(status != IDLE)
    {
	DPSRemovePort(reply_port);
        if(play_mon) DPSRemoveTimedEntry(play_mon);
	play_mon = 0;
	SNDRelease(SND_ACCESS_OUT|SND_ACCESS_DSP,dev_port,owner_port); 
	vm_deallocate(task_self(),(pointer_t)foo, vm_page_size);
	vm_deallocate(task_self(),(pointer_t)faa, 4*DMASIZE);
	k_err = port_deallocate(task_self(),reply_port);
	free(filtre);
	status = IDLE;
	if (delegate  && [delegate respondsTo:@selector(didPlay:)])
	    [delegate perform:@selector(didPlay:) with:self];
	if (delegate  && [delegate respondsTo:@selector(soundAnimate:)])
	    [delegate perform:@selector(soundAnimate:) with:(id)20];
	return self;
    }
    return self;
}

-(int) pause
{

    if(AES != 0 || status == IDLE) return 0;    
    switch(status)
    {
	case PLAYING :
	    k_err = snddriver_dsp_host_cmd(cmd_port,21,SNDDRIVER_LOW_PRIORITY);
	    Error(k_err,"Couldn't pause");
	    if(play_mon) DPSRemoveTimedEntry(play_mon);
	    play_mon = 0;
	    status = PAUSED; break;
	case PAUSED :
	    k_err = snddriver_dsp_host_cmd(cmd_port,20,SNDDRIVER_LOW_PRIORITY);
	    Error(k_err,"Couldn't resume");
	    play_mon = DPSAddTimedEntry(0.05, (DPSTimedEntryProc) PlayMonitor,
	    	 self, NX_MODALRESPTHRESHOLD);
	    status = PLAYING; break;
	default : return 0;
    }
    return 1;
}

- pause:sender
{
    [self pause];
    return self;
}


- playCompleted:tag
{
    usleep(100000);
    [self stop];
//    if (delegate  && [delegate respondsTo:@selector(didPlay:)])
//	[delegate perform:@selector(didPlay:) with:self];
    return self;
}

- playMonitor
{
int samples;
int isStereo = (stereo+1);
int del = (delegate  && [delegate respondsTo:@selector(soundAnimate:)]);

    k_err = snddriver_stream_nsamples(write_port, &samples);
    samples /= sizeof(short); // samples originally contains nb of bytes!
    if (del)
    {
    if(samples/isStereo > size_samp)
    [delegate perform:@selector(soundAnimate:) with:0];
    else
    [delegate perform:@selector(soundAnimate:) 
    	with:(id)((first_samp+samples)/isStereo)];
    }
    return self;
}

- setWait:(BOOL)wait
{
    WAIT = wait;
    return self;
}

- setGain:(float)aGain
{
    gain = aGain;
    return self;
}
   
@end