/*
 * DSPRecorder.m
 * Implementation of a DSP sound recording object.
 * Author: Robert D. Poor, NeXT Technical Support
 * Copyright 1989 NeXT, Inc.  Next, Inc. is furnishing this software
 * for example purposes only and assumes no liability for its use.
 *
 * Edit history (most recent edits first)
 *
 *  4-Feb-90 Rob Poor: cleanup the API.
 * 21-Dec-89 Rob Poor: Hack to increase the sounddriver timeout from 
 *	1 second to 10 seconds.  Guaranteed not to work in new shlibs.
 * 11-Dec-89 Rob Poor: Don't deallocate the ownerPort, which is the result
 *	of netname_look_up.
 * 06-Dec-89 Rob Poor: Deleted the unused and useless +newSetup method.
 * 05-Dec-89 Rob Poor: Changed several instances of check_mach_error to 
 *	check_snddriver_error.  Put up an alert panel for receive timeouts
 *	in updateStream (rather than simply calling exit).
 * 11-Sep-89 Rob Poor: In recorderResume, unpause the stream before
 *	sending down the DMA size to avoid losing initial DSP data.
 * 07-Sep-89 Rob Poor: Created.
 *
 * End of edit history
 */

/*
 * The DSPRecorder object records data from the DSP chip.  It handles
 * all the setup and sequencing required to receive buffers of data
 * from the DSP, which will typically be running a program to read
 * sound samples from the external DSP port.
 *
 * With no delegate methods installed, the DSPRecorder won't do
 * anything useful; it will merely recieve buffers of data from the
 * DSP and throw them away.  However, the following three delegate
 * methods are implemented:
 *	willRecord :recorder
 *		This method is called after all DSP resources have
 *		been allocated but before any recording starts.  You
 *		might use this to open an output file for recording.
 *	didRecord :recorder
 *		This method is called after recording stops.  You
 *		might use this method for closing the output file.
 *	recordData :recorder :data :nbytes
 *		This method is called whenever another buffer of data
 *		is available from the DSP.  You might use this method
 *		to write the incoming data to a sound file.
 */

#import <stdio.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 "DSPRecorder.h"
#import "errors.h"

/*
 * EL GRANDE HACKOLA: There is a minor bug in the current sounddriver
 * library: the static int "timeout", defined in the snddriver library,
 * is demonstrably too short at 1000 (= 1 second).  A timeout of 10000
 * (= 10 seconds) works without any observed problems.  We know the address
 * of the timeout in the current shared library (found it with gdb), so we
 * patch it here on the fly.  THIS CODE WILL BREAK WHEN THE SHARED LIBRARY
 * CHANGES. Our only hedge for now is to make sure that the address in question
 * really contains 1000 before we change it.
 */
#define TIMEOUT_ADDR	((int *)0x40116ec)
#define TIMEOUT_OLDVAL	1000
#define TIMEOUT_NEWVAL	10000

static void fix_snddriver_timeout()
{
  if (*TIMEOUT_ADDR != TIMEOUT_OLDVAL) {
     NXRunAlertPanel(NULL,"Can't correct timeout bug","OK",NULL,NULL);
  } else {
    *TIMEOUT_ADDR = TIMEOUT_NEWVAL;
  }
}
/* end of EL GRAND HACKOLA */


/* 
 * HandleDSPMessage is called courtesy of the DPSAddPort mechanism
 * whenever a message arrives from the DSP driver.  userData is bound
 * to the DSPRecorder object itself.
 */
static void HandleDSPMessage(msg_header_t *msg, void *userData)
{
  int k_err;
  snddriver_handlers_t *handlers;

  handlers = [(DSPRecorder *)userData msgHandlers];
  /* handlers = (DSPRecorder *)userData->msgHandlers; */
  k_err = snddriver_reply_handler(msg, handlers);
  check_snddriver_error(k_err,"Cannot parse message from DSP");
}

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

static void DSPRecordedDataMsg(void *arg, int tag, char *data, int nbytes)
{
  [(id)arg recordedData:tag:data:nbytes];
}

/*
 * The dispatch table that snddriver_reply_handler uses.
 */
const static snddriver_handlers_t dspHandlers = {
  (void *)0,
  (int) 0,
  NULL,			/* StartedMsg */
  NULL,			/* CompletedMsg */
  NULL,			/* AbortedMsg */
  NULL,			/* PausedMsg */
  NULL,			/* ResumedMsg */
  NULL,			/* OverflowMsg */
  DSPRecordedDataMsg,
  NULL,
  NULL,
  NULL
  };
  
@implementation DSPRecorder:Recorder

/*
 * Create a new DSPRecorder object.
 */
+ new
{
  self = [super new];

  msgHandlers = dspHandlers;	/* Copy the static structure */
  msgHandlers.arg = self;	/* Install self as arg to handler functions */

  fix_snddriver_timeout();

  return self;
}

- prepare
/*
 * prepare the recorder for recording.  It grabs all the requisite resources
 * and leaves the record stream in a paused state.
 */ 
{
  port_t arbitration_port;
  int r, protocol;

  [self stop];		/* make sure that we are stopped first */

  bytesRecorded = 0;

  /* get the device port for the sound/dsp driver on the local machine */
  r = netname_look_up(name_server_port,"","sound", &devicePort);
  check_mach_error(r,"netname lookup failure");

  /* try to become owner of the dsp resource */
  r = port_allocate(task_self(), &ownerPort);
  check_mach_error(r,"Cannot allocate owner port");
  arbitration_port = ownerPort;
  r = snddriver_set_dsp_owner_port(devicePort,ownerPort,&arbitration_port);
  check_snddriver_error(r,"Cannot become owner of dsp resources");

  /* get the command port */
  r = snddriver_get_dsp_cmd_port(devicePort,ownerPort,&commandPort);
  check_mach_error(r,"Cannot acquire command port");

  /* set up the DMA read stream and set the DSP protocol */
  protocol = 0;
  r = snddriver_stream_setup(devicePort,
			     ownerPort,
			     SNDDRIVER_STREAM_FROM_DSP,
			     READ_BUF_SIZE,
			     BYTES_PER_16BIT,
			     LOW_WATER,
			     HIGH_WATER,
			     &protocol,
			     &streamPort);
  check_snddriver_error(r,"Cannot set up stream from DSP");
  r = snddriver_dsp_protocol(devicePort, ownerPort, protocol);
  check_snddriver_error(r,"Cannot set up DSP protocol");

  /* allocate a port for the replies */
  r = port_allocate(task_self(),&replyPort);
  check_mach_error(r,"Cannot allocate reply port");

  /* Start the DMA stream in a paused state */
  r = snddriver_stream_control(streamPort,0,SNDDRIVER_PAUSE_STREAM);
  check_snddriver_error(r,"can't do initial pause");

  /* Queue up the initial DMA buffers before starting */
  bytesEnqueued = 0;
  [self updateStream];

  DPSAddPort(replyPort,		
	     HandleDSPMessage,		/* function to call */
	     MSG_SIZE_MAX,	
	     self,			/* first arg to HandleDSPMessage */
	     NX_RUNMODALTHRESHOLD	/* priority */
	     );

  /* 
   * Tell the delegate (if any) that we are about to start recording.
   */
  if (delegate && [delegate respondsTo:@selector(willRecord:)]) {
    [delegate willRecord :self];
  }

  recorderState = REC_PAUSED;
  /*
   * Common sense would dictate that we should boot the DSP program
   * here, but if we do, we will get a little blip of garbage at the
   * start of the recording.  Instead, we boot the DSP when we resume
   * the stream.
   */

  return self;
}

/*
 * recorderResume is only be called from a PAUSED state.  It
 * downloads the DSP program and resumes the DMA stream.
 */
- run
/*
 * Start (or resume) the recording.  If the recorder is already running, this
 * method has no effect.  If the recorder is in a stopped state, it will call
 * prepare first to set up the recorder.
 */
{
  int r;
  int bufsize = READ_BUF_SIZE;

  if (recorderState == REC_RUNNING) {
    return nil;
  } else if (recorderState == REC_STOPPED) {
    [self prepare];
  } /* else recorderState == REC_PAUSED */

  /* boot the dsp from the dspProgram sound structure. */
  r = SNDBootDSP(devicePort, ownerPort, dspProgram);
  check_snd_error(r,"Failed to boot DSP");

  /* Resume the DMA stream. */
  r = snddriver_stream_control(streamPort,0,SNDDRIVER_RESUME_STREAM);
  check_snddriver_error(r,"Can't resume the DMA stream");

  /*
   * communicate the read dma buffer size to the DSP (the dsp program
   * reads this in its reset routine).
   */
  r = snddriver_dsp_write
    (commandPort,		/* port */
     &bufsize,			/* pointer to the data to write */
     1,				/* count of data elements */
     sizeof(int),		/* bytes per data element */
     SNDDRIVER_MED_PRIORITY	/* priority of this transactions */
     );
  check_snddriver_error(r,"Cannot set up DSP");

  /*
   * Post a request to AWAIT_STREAM, which will cause a recordedData
   * message to be delivered sooner rather than later.
   */
  r = snddriver_stream_control(streamPort, 
			       READ_TAG,
			       SNDDRIVER_AWAIT_STREAM);
  check_snddriver_error(r,"Call to await stream failed");

  recorderState = REC_RUNNING;
  return self;
}

- pause
/*
 * pause will suspend the recording in such a way that run will resume it.
 * If called from the stopped state, pause will prepare the stream.  If
 * called from a paused state, this method has no effect.
 */
{
  int r;

  if (recorderState == REC_PAUSED) {
    return nil;
  } else if (recorderState == REC_STOPPED) {
    return [self prepare];
  } /* else recorderState == REC_RUNNING */

  r = snddriver_stream_control(streamPort, 
			       READ_TAG,
			       SNDDRIVER_PAUSE_STREAM);
  check_snddriver_error(r,"Call to pause stream failed");

  recorderState = REC_PAUSED;
  return self;
}

- stop
/*
 * Stop recording, shut down the DSP, flush outstanding buffers, frees the
 * resources acquired in prepare.  New state becomes REC_STOPPED,
 */
{
  int r;

  if (recorderState == REC_STOPPED) return nil;
  
  recorderState = REC_STOPPED;

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

  r = port_deallocate(task_self(),ownerPort);
  check_mach_error(r,"Couldn't deallocate ownerPort");

  /* 
   * Tell the delegate (if any) that we stopped recording.
   */
  if (delegate && [delegate respondsTo:@selector(didRecord:)]) {
    [delegate didRecord :self];
  }

  return self;
}

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

- setDspProgram :(SNDSoundStruct *)dsp_program
{
  dspProgram = dsp_program;
  return self;
}

- (SNDSoundStruct *)dspProgram
{
  return dspProgram;
}

- (snddriver_handlers_t *)msgHandlers
{
  return &msgHandlers;
}

/*
 * recordedData is where most of the recording happens.  Whenever we
 * get a handleRecordedData message, we write what we've got to a file,
 * free the recorded data buffer, and instantly post a request for
 * another recorded data message (via the AWAIT_STREAM control message).
 */
- recordedData :(int)tag :(char *)data :(int)nbytes
{
  int r;

  bytesRecorded += nbytes;
  bytesEnqueued -= nbytes;

  if (delegate && [delegate respondsTo:@selector(recordData:::)]) {
    [delegate recordData:self:data:nbytes];
  }

  /* Free up the virtual memory used in the data buffer */
  r = vm_deallocate(task_self(), (pointer_t)data, nbytes);
  check_mach_error(r,"Failed to deallocate data buffer");

  /*
   * The delegate of recordData may have stopped the recording, so we
   * check the state before queueing up new read buffers, etc.
   */
  if (recorderState == REC_RUNNING) {
    /* Allocate a new read buffer */
    [self updateStream];
    /* and then instantly request any additional data that's come in. */
    r = snddriver_stream_control(streamPort, 
				 READ_TAG,
				 SNDDRIVER_AWAIT_STREAM);
    check_snddriver_error(r,"Call to await stream failed");
  }

  return self;
}

/*
 * updateStream enqueues one or more read requests messages.
 */
- updateStream
{
  int r;

  while (bytesEnqueued <= REGION_SIZE) {
    r = snddriver_stream_start_reading
      (streamPort,			/* port */
       0,				/* backing store (not yet impl'd) */
       REGION_SIZE/BYTES_PER_16BIT,	/* count (in samples) to read */
       READ_TAG,			/* user data */
       FALSE,				/* send msg when started */
       FALSE,				/* send msg when completed */
       FALSE,				/* send msg when aborted */
       FALSE,				/* send msg when paused */
       FALSE,				/* send msg when resumed */
       FALSE,				/* send msg when overflowed */
       replyPort			/* port for the above messages */
       );
    check_snddriver_error(r,"Cannot enqueue read request");
    bytesEnqueued += REGION_SIZE;
  }
  return self;
}

@end


