/*
 * DACPlayer.m
 * Implementation of an object to play sound over the soundout device (DACs).
 * 
 * You may freely copy, distribute, and reuse the code in this example.
 * NeXT disclaims any warranty of any kind, expressed or  implied, as to its
 * fitness for any particular use.
 *
 * Written by: Robert Poor
 * Edit History (most recent edits first):
 *
 * Version II created: Sep/92
 * Version I created Dec/89
 *
 * End of Edit History
 */

#import <mach.h>
#import <servers/netname.h>
#import <appkit/Application.h>
#import <appkit/Panel.h>
#import <sound/accesssound.h>
#import <sound/sounddriver.h>
#import <sound/soundstruct.h>
#import "DACPlayer.h"
#import "errors.h"
#import <c.h>		/* for MAX */

@interface DACPlayer(DACPlayerPrivate)
- _setState:(Pla_state_t)newState;
- _updateParameters;
- _enqueueRegions;
- _completedRegion;
@end

/*
 * Following are the routines that snddriver_reply_handler will
 * dispatch to. Note that in each case, the DACPlayer object itself
 * will be passed as the first argument.
 */

static void DACCompletedMsg(void *arg, int tag)
{
  [(id)arg _completedRegion];
}

/*
 * The dispatch table that snddriver_reply_handler uses.
 *
 * ### rpoor Sep 92: It appears that the sound driver doesn't post
 * ### underrun messages.  I tried everything and never got notified...
 */
static snddriver_handlers_t dacHandlers = {
  (void *)0,		/* user-defined arg passed to dispatch functions */
  (int) 0,		/* timeout */
  NULL,			/* DACStartedMsg */
  DACCompletedMsg,
  NULL,			/* DACAbortedMsg */
  NULL,			/* DACPausedMsg */
  NULL,			/* DACResumedMsg */
  NULL,			/* DACUnderrunMsg */
  NULL,
  NULL,
  NULL,
  NULL
  };
  
/* 
 * HandleDACMessage is called courtesy of the DPSAddPort mechanism
 * whenever a message arrives from the sound driver.  userData is bound
 * to the DACPlayer object itself.
 */
static void HandleDACMessage(msg_header_t *msg, void *userData)
{
  int r;

  dacHandlers.arg = userData;	/* Install dacPlayer as arg to handlers */
  r = snddriver_reply_handler(msg, &dacHandlers);
  checkMachError((id)dacHandlers.arg, r, "Can't parse message from DAC");
}


@implementation DACPlayer:Object

- init
{
  self = [super init];

  playerState = PLA_STOPPED;
  regionsQueued = bytesPlayed = bytesQueued = 0;
  [self setSamplingRate:SND_RATE_HIGH];
  [self setRegionSize:4*vm_page_size andCount:3];

  return self;
}

- free
{
  return [super free];
}

/***
 *** METHODS THAT CONTROL DACPLAYER
 ***/

- prepare
/*
 * grab all the required resources, queue up the initial regions
 * (this WILL call the delegate playData::: method), and set state
 * to PLA_PAUSED.
 */
{
  port_t arbitration_port;
  int r, rate, protocol;

  [self stop];			/* make sure playing has stopped first. */

  /* get the ports that we need */
  r = SNDAcquire(SND_ACCESS_OUT, 0, 0, 0, NULL_NEGOTIATION_FUN,
		  0, &devicePort, &ownerPort);
  if (!checkMachError(self,r,"Can't acquire SoundOut resouces"))
    return nil;

  arbitration_port = ownerPort;
  r = snddriver_set_sndout_owner_port(devicePort,ownerPort,&arbitration_port);
  if (!checkSnddriverError(self,r,"Cannot become owner of sound-out resources"))
    return nil;

  r = snddriver_set_ramp(devicePort,0);		/* disable ramping */
  checkSnddriverError(self,r,"Call to disable ramp failed");	/* not fatal */

  /* 
   * Tell the delegate (if any) that we are about to start playing.
   * Call it here in case the delegate wants to configure any of the
   * dacPlayer parameters (samplingRate, regionSize, regionCount).
   */
  if (flags.willPlay) {
    [delegate willPlay :self];
  }
  [self _updateParameters];

  /* set up the DMA read stream  */
  protocol = 0;
  rate = ((samplingRate == SND_RATE_HIGH)?
	SNDDRIVER_STREAM_TO_SNDOUT_44:
	SNDDRIVER_STREAM_TO_SNDOUT_22);
  r = snddriver_stream_setup(devicePort,
			     ownerPort,
			     rate,
			     READ_BUF_SIZE,
			     sizeof(short),
			     LOW_WATER,
			     HIGH_WATER,
			     &protocol,		/* ignored for sndout */
			     &streamPort);
  if (!checkSnddriverError(self,r,"Cannot set up stream to sound-out"))
    return nil;

  /* allocate a port for the replies */
  r = port_allocate(task_self(),&replyPort);
  if (!checkMachError(self,r,"Cannot allocate reply port"))
    return nil;

  /* Start the DMA stream in a paused state */
  r = snddriver_stream_control(streamPort,0,SNDDRIVER_PAUSE_STREAM);
  if (!checkSnddriverError(self,r,"can't do initial pause"))
    return nil;

  /*
   * Queue up the initial buffers before starting.
   */
  bytesPlayed = 0;
  bytesQueued = 0;
  regionsQueued = 0;
  [self _enqueueRegions];

  /*
   * Set things up to call HandleDACMessage whenever we receive a
   * message on the replyPort.  (We don't expect ro recieve any message
   * until we unpause the stream, see the -run method for that...)
   */
  DPSAddPort(replyPort,		
	     HandleDACMessage,		/* function to call */
	     MSG_SIZE_MAX,	
	     self,			/* second arg to HandleDACMessage */
	     NX_MODALRESPTHRESHOLD	/* priority */
	     );

  [self _setState:PLA_PAUSED];

  return self;
}

- run
{
  int r;

  if (playerState == PLA_RUNNING) {
    return nil;			/* already running.  Ignore the bum */
  } else if (playerState == PLA_STOPPED) {
    [self prepare];		/* starting cold.  prepare first then run */
  } else if (playerState == PLA_STOPPING) {
    [[self stop] prepare];	/* stop first, prepare, then run... */
    return nil;
  } /* else playerState == PLA_PAUSED */

  /*
   * At this point we know that state = PLA_PAUSED, any initial buffers
   * are already queued up, and the stream is paused.  We simply resume
   * the stream to start things going...
   */
  r = snddriver_stream_control(streamPort,0,SNDDRIVER_RESUME_STREAM);
  if (!checkSnddriverError(self,r,"Can't resume the DMA stream"))
    return nil;

  return [self _setState:PLA_RUNNING];
}

- pause
{
  int r;

  if (playerState == PLA_PAUSED) {
    return nil;
  } else if (playerState == PLA_STOPPED) {
    return [self prepare];
  } else if (playerState == PLA_STOPPING) {
    return [[self stop] prepare];
  } /* else playerState == PLA_RUNNING */

  r = snddriver_stream_control(streamPort,WRITE_TAG,SNDDRIVER_PAUSE_STREAM);
  checkSnddriverError(self,r,"Call to pause stream failed");

  return [self _setState:PLA_PAUSED];
}

- stop
/*
 * Terminate all playing right now.
 */
{
  int r;

  if (playerState == PLA_STOPPED) {
    return nil;
  }

  /* flush any outstanding buffers */
  r = snddriver_stream_control(streamPort,
			       WRITE_TAG,
			       SNDDRIVER_ABORT_STREAM);
  checkSnddriverError(self,r,"Couldn't abort stream");
  DPSRemovePort(replyPort);

  r = SNDRelease(SND_ACCESS_OUT, devicePort, ownerPort);
  if (!checkMachError(self,r,"Can't deallocate the owner port"))
    return nil;
  r = port_deallocate(task_self(),replyPort);
  if (!checkMachError(self,r,"Can't deallocate the reply port"))
    return nil;

  /* 
   * Tell the delegate that we stopped playing.
   */
  if (flags.didPlay) {
    [delegate didPlay :self];
  }

  bytesPlayed = bytesQueued = 0;	/* we've shut everything down */
  return [self _setState:PLA_STOPPED];
}


- finish
/*
 * The finish method only sets the state to PLA_STOPPING.  The routines
 * that handle requests from the sound driver will notice this and will
 * stop enqueuing new regions.  When the last region has been played,
 * dacPlayer will call [self stop] to shut things down.
 */
{
  if (playerState == PLA_STOPPED) {
    return nil;			/* already stopped */
  } else if (playerState == PLA_STOPPING) {
    return nil;			/* already stopping */
  } /* else (playerState == PLA_RUNNING) || (playerState == PLA_PAUSED) */
  
  return [self _setState:PLA_STOPPING];
}


/***
 *** METHODS THAT CONFIGURE DACPLAYER
 ***/

- delegate { return delegate; }
- setDelegate:anObject
{
  delegate = anObject; 
  /* make note of which methods the delegate responds do */
  flags.willPlay = [delegate respondsTo:@selector(willPlay:)];
  flags.didPlay = [delegate respondsTo:@selector(didPlay:)];
  flags.playData = [delegate respondsTo:@selector(playData:::)];
  flags.didChangeState = 
    [delegate respondsTo:@selector(didChangeState:from:to:)];
  return self;
}

- (int)regionSize { return regionSize; }
- (int)regionCount { return regionCount; }
- setRegionSize:(int)size andCount:(int)count
{
  int pages;

  /* round up to nearest vm_page_size boundary */
  pages = (size+vm_page_size-1)/vm_page_size;
  newRegionSize = MAX(1,pages)*vm_page_size;
  newRegionCount = MAX(1,count);
  /* update the new parameters if it's safe... */
  return [self _updateParameters];
}

- (int)samplingRate { return samplingRate; }
- setSamplingRate:(int)aRate
{
  if (aRate < (SND_RATE_HIGH+SND_RATE_LOW)/2) {
    newSamplingRate = SND_RATE_LOW;
  } else {
    newSamplingRate = SND_RATE_HIGH;
  }
  /* update the new parameters if it's safe... */
  return [self _updateParameters];
}

/***
 *** METHODS THAT QUERY DACPLAYER
 ***/

- (Pla_state_t)playerState { return playerState; }

#define BYTES_PER_SAMPLE	(sizeof(short))
#define	BYTES_PER_FRAME		(BYTES_PER_SAMPLE * 2)

- (int)bytesPlayed { return bytesPlayed; }
- (int)samplesPlayed { return bytesPlayed / BYTES_PER_SAMPLE; }
- (int)framesPlayed { return bytesPlayed / BYTES_PER_FRAME; };
- (double)secondsPlayed
{
  return (double)bytesPlayed / (double)(samplingRate * BYTES_PER_FRAME);
}

- (int)bytesQueued { return bytesQueued; }
- (int)samplesQueued { return bytesQueued / BYTES_PER_SAMPLE; }
- (int)framesQueued { return bytesQueued / BYTES_PER_FRAME; }
- (double)secondsQueued
{
  return (double)bytesQueued / (double)(samplingRate * BYTES_PER_FRAME);
}


/***
 *** Internal methods
 ***/

- _setState:(Pla_state_t)newState
/*
 * Set the state to newState.  If there's a delegate that wants to know
 * about the change in state, tell 'em.  Returns self.
 */
{
  Pla_state_t prevState = playerState;

  playerState = newState;
  if (flags.didChangeState) {
    [delegate didChangeState:self from:prevState to:newState];
  }
  return self;
}

- _updateParameters
/*
 * If the state == PLA_STOPPED, update the "pending" new parameters.
 * We'll always call this method in -prepare before starting.
 */
{
  if (playerState == PLA_STOPPED) {
    samplingRate = newSamplingRate;
    regionCount = newRegionCount;
    regionSize = newRegionSize;
  }
  return self;
}

- _enqueueRegions
/*
 * This is the method that sends buffers to the sound driver stream.
 *
 * Unless state == STOPPING, it writes regions to the sound driver until
 * regionsQueued == regionCount.  Each region is passed to the delegate
 * in a playData::: method before being handed off to the sound driver.
 */
{
  int r;
  char *currentRegion;

  while ((playerState != PLA_STOPPING) && (regionsQueued < regionCount)) {

    /* allocate a region */
    r = vm_allocate(task_self(),&(vm_address_t)currentRegion,regionSize,TRUE);

    /* let the delegate have its way with the regrion */
    if (flags.playData) {
      [delegate playData :self :currentRegion :regionSize];
    }
    
    /* enqueue the buffer in the DMA stream to the DACs */
    r = snddriver_stream_start_writing
	(streamPort,		/* port */
	  currentRegion,	/* data to be played */
	  regionSize/sizeof(short),	/* number of samples to play */
	  WRITE_TAG,		/* tag for this region */
	  FALSE,		/* preempt */
	  TRUE,			/* deallocate on completion */
	  FALSE,		/* send msg when started */
	  TRUE,			/* send msg when completed */
	  FALSE,		/* send msg when aborted */
	  FALSE,		/* send msg when paused */
	  FALSE,		/* send msg when resumed */
	  FALSE,		/* send msg when underflowed */
	  replyPort		/* port for the above messages */
	  );
    checkSnddriverError(self,r,"Cannot enqueue write request");
    regionsQueued += 1;
    bytesQueued += regionSize;
  }
  return self;
}

- _completedRegion
/*
 * This method is called whenever we get a "region completed" message from
 * the sound driver.  (How?  The sound driver posts a message on replyPort,
 * which is noticed by the DPSAddPort mechanism which calls HandleDACMessage
 * which calls snddriver_reply_handler which dispatches to DACCompletedMessage
 * which (finally) calls _completedRegion.)  Got that?
 *
 * Oh, what's this method DO?  It makes note of the fact that another region
 * has been consumed by the DAC stream and calls _enqueueRegions to replenish
 * the region queue.  If state == PLA_STOPPING and the region queue has hit
 * zero, it means that we've just played the last region and we call 
 * [self stop] to shut things down.
 */
{
  /* bump bytesPlayed to reflect the fact that the sound driver played 'em */
  bytesPlayed += regionSize;

  /* note that one region has been consumed by the sound driver. */
  regionsQueued -= 1;
  
  /* replenish the queue of regions */
  [self _enqueueRegions];

  /* shut down the machinery if we're stopping */
  if ((playerState == PLA_STOPPING) && (regionsQueued == 0)) {
    [self stop];
  }
  return self;
}


@end
