/*
 * Transport.m
 * "Dime store controller" for a DSPRecorder 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)
 *
 * 12-Dec-89 R. Dunbar Poor: Added a play button.
 * 05-Dec-89 R. Dunbar Poor: added support for 22.05KHz and Mono recording.
 *	DSP program is loaded from machO segment rather than external file.
 * 11-Sep-89 Rob Poor: In willRecord, lseek rather than ftruncate
 *	to make room for the sound file header.
 * 07-Sep-89 Rob Poor: Created.
 *
 * End of edit history
 */

/*
 * The Transport object provides a very simple user interface to the
 * DSPRecorder object.  It provides:
 * 	stop/pause/start buttons
 *	a window in which to specify the output (sound) file
 *	a window in which to specify the DSP program to load
 *	a running status window, updated every second.
 *
 * The Transport object installs itself as the delegate for the
 * DSPRecorder object.  The delegate methods (willRecord, recordData,
 * and didRecord), open a file, write the sound samples to that file,
 * and close the file at the end of recording.
 */

#import <libc.h>
#import <appkit/Application.h>
#import <appkit/Matrix.h>
#import <appkit/TextField.h>
#import <appkit/SavePanel.h>
#import <soundkit/Sound.h>
#import <sound/filesound.h>
#import "Transport.h"
#import "DSPRecorder.h"
#import "errors.h"

/*
 * UpdateStatus is called via a timed entry every second.  It simply
 * dispatches to the updateStatus method for the Transport object.
 */
void UpdateStatus (DPSTimedEntry te, double timeNow, void *data)
{
  [(id)data updateStatus];
}

@implementation Transport:Object

+ new
{
  self = [super new];
  outFileFD = FD_CLOSED;

  /* Create a timed entry to update the status window every second. */
  statusTE = DPSAddTimedEntry(1.0, &UpdateStatus, self, NX_BASETHRESHOLD);

  /* Allocate a DSP recorder and install self as the delegate. */
  dspRecorder = [DSPRecorder new];
  [dspRecorder setDelegate: self];

  return self;
}

/*
 * Standard methods required by the Interface Builder for initializing
 * the instance variables.
 */
- setFileNameWindow:anObject
{
  fileNameWindow = anObject;
  return self;
}

- setStatusWindow:anObject
{
  statusWindow = anObject;
  return self;
}

- setMonoStereoButtons:anObject
{
  monoStereoButtons=anObject;
  return self;
}

- setSamplingRateButtons:anObject
{
  samplingRateButtons=anObject;
  return self;
}

- (SavePanel *)savePanel
{
  if (savePanel == nil) savePanel = [SavePanel new];
  return savePanel;
}

- openOutput:sender 
/*
 * Get the name for the output file.  Doesn't actually open it; that
 * happens in willRecord (qv).
 */
{
  [self closeOutput:self];

  if ([[self savePanel] runModalForDirectory:"/tmp" file:"temp.snd"]) {
    outFilename = [savePanel filename];
    [fileNameWindow setStringValue:outFilename];
  }
  return self;
}

- closeOutput :sender
{
  outFilename = NULL;
  [fileNameWindow setStringValue:""];

  return self;
}

- closeOutputFD :sender
{
  if (outFileFD != FD_CLOSED) {
    close(outFileFD);
    outFileFD = FD_CLOSED;
  }
  return [self updateStatus];
}

- stop:sender
{
  if (playSound) {
    [playSound stop];
  } else {
    [dspRecorder stop];
  }
  return [self updateStatus];
}

- pause:sender
{
  if (playSound) {
    [playSound pause];
  } else {
    [dspRecorder pause];
  }
  return [self updateStatus];
}

- play:sender
{
  [dspRecorder stop];
  if (!playSound) {
    playSound = [Sound newFromSoundfile:(char *)outFilename];
  }
  [playSound play];
  return [self updateStatus];
}

- record:sender
{
  if (playSound) {
    [playSound stop];
    [playSound free];
    playSound = nil;
  }
  [dspRecorder run];
  return [self updateStatus];
}

/*
 * Update the status display.  Called every second via a timed entry. 
 */
- updateStatus
{
  int state = [dspRecorder state];
  int nbytes = [dspRecorder bytesRecorded];
  char *stateName;
  char msg[100];

  if (state == REC_STOPPED) stateName =		"stopped";
  else if (state == REC_PAUSED) stateName =	"paused";
  else if (state == REC_RUNNING) stateName =	"running";
  else stateName = 				"unknown";

  sprintf(msg,"%s, %d bytes read\n",stateName,nbytes);
  [statusWindow setStringValue:msg];

  return self;
}

/*
 * Delegate methods for the DSPRecorder object.
 */

/*
 * The delegate method willRecord is called whenever the DSPRecorder
 * is about to start recording.  At this time, we open a file which
 * will be used to record the sound into.  We also read a DSP program
 * which the DSPRecorder will use to boot the DSP.
 */
- willRecord :recorder
{
  char *filename;
  Sound *dspProgram;
  int s_err, u_err;

  if (outFilename == NULL) [self openOutput:self];

  /* outFileFD should never be open when we get here... */
  if (outFileFD != FD_CLOSED) {
    NXRunAlertPanel(NULL,"Ouput file is already open??!?","OK",NULL,NULL);
    [dspRecorder stop];
    [self closeOutputFD:self];
    return nil;
  }

  outFileFD = open(outFilename,O_WRONLY|O_TRUNC|O_CREAT,0666);
  if (outFileFD < 0) {
    NXRunAlertPanel(NULL,"Could not open output sound file.","OK",NULL,NULL);
    [dspRecorder stop]; 
    return nil;
  }

  monoStereo = [[monoStereoButtons selectedCell] tag];
  samplingRate = [[samplingRateButtons selectedCell] tag];
  [monoStereoButtons setEnabled:NO];
  [samplingRateButtons setEnabled:NO];

  /* Leave a hole at the start for a sound file header ... */
  u_err = lseek(outFileFD, HEADER_SIZE, L_SET);
  if (u_err == -1) {
    NXRunAlertPanel(NULL,"No output sound file is open.","OK",NULL,NULL);
    [dspRecorder stop]; 
    return nil;
  }

  dspProgram = [Sound newFromMachO:"dsprecord.snd"];
  if (dspProgram == nil) {
    NXRunAlertPanel(NULL,"Can't load the DSP program","OK",NULL,NULL);
    [dspRecorder stop]; 
    return nil;
  }
  [dspRecorder setDspProgram:[dspProgram soundStruct]];

 return self;
}

/*
 * The delegate method didRecord is called whenever the DSPRecorder
 * finished recording.  At this time, we add a sound file header onto
 * the open sound file and then close the file.
 */
- didRecord :recorder
{
  int u_err, s_err, bytesRecorded;
  SNDSoundStruct SNDHeader;

  bytesRecorded = [dspRecorder bytesRecorded];
  if (monoStereo != STEREO_TAG) {
    /* We've mixed stereo down to mono */
    bytesRecorded = bytesRecorded/2;
  }
  if (samplingRate == SRATE_22K_TAG) {
    /* We've tossed every other sample to reduce the srate */
    bytesRecorded = bytesRecorded/2;
  }

  /* Add a header to the file */
  SNDHeader.magic = SND_MAGIC;
  SNDHeader.dataLocation = HEADER_SIZE; /* right after header */
  SNDHeader.dataSize = bytesRecorded;
  SNDHeader.dataFormat = SND_FORMAT_LINEAR_16;
  SNDHeader.samplingRate = 
    (samplingRate==SRATE_22K_TAG)?SND_RATE_LOW:SND_RATE_HIGH;
  SNDHeader.channelCount = (monoStereo==STEREO_TAG)?2:1;

  u_err = lseek(outFileFD, 0, L_SET);
  if (u_err == -1) {
    NXRunAlertPanel(NULL,"Can't seek for sound file header.","OK",NULL,NULL);
    return nil;
  }
  u_err = write(outFileFD, (char *)&SNDHeader, sizeof(SNDHeader));
  if (u_err == -1) {
    NXRunAlertPanel(NULL,"Can't write sound file header.","OK",NULL,NULL);
    return nil;
  }
  /* close the file */
  [self closeOutputFD:self];

  [monoStereoButtons setEnabled:YES];
  [samplingRateButtons setEnabled:YES];

  return self;
}

/*
 * recordData::: will be called whenever we get a new buffer of data
 * from the DSP.  We modify the data buffer in-place depending on the
 * mono/stereo and the sample rate settings before writing the buffer
 * out.
 */
- recordData :recorder :(char *)data :(int)nbytes
{
  register short *get, *put;
  register int i, write_samples;

  get = put = (short *)data;
  write_samples = nbytes/sizeof(short);

  switch (monoStereo) {
  case STEREO_TAG:
    if (samplingRate==SRATE_22K_TAG) {
      /* Stereo, 22K: decimate by 2 (discard alternate samples) */
      write_samples /= 2;
      for (i=write_samples/2; i>0; i--) {
        *put++ = *get++;		/* left channel */
	*put++ = *get++;		/* right channel */
	get += 2;			/* skip a sample frame */
      }
    } else {
      /* Stereo, 44K: No decimation or mixing needed. */
    }
    break;
  case MONO_MIX_TAG:
    /* mix left and right channels */
    if (samplingRate==SRATE_22K_TAG) {
      /* Mono Mix, 22K: decimate by 2, mix left and right channels */
      write_samples /= 4;
      for (i=0;i<write_samples;i++) {
	*put++ = (*get++ + *get++)/2;
	get += 2;
      }
    } else {
      /* Mono Mix, 44K: no decimation, mix left and right channels */
      write_samples /= 2;
      for (i=0;i<write_samples;i++) {
	*put++ = (*get++ + *get++)/2;
      }
    }
    break;
  case MONO_L_TAG:
    if (samplingRate==SRATE_22K_TAG) {
      /* Mono Left, 22K: decimate by 2, only keep the left channel. */
      write_samples /= 4;
      for (i=0;i<write_samples;i++) {
	*put++ = *get;
	get += 4;
      }
    } else {
      /* Mono Left, 44K: no decimation, only keep the left channel. */
      write_samples /= 2;
      for (i=0;i<write_samples;i++) {
	*put++ = *get;
	get += 2;
      }
    }
    break;
  case MONO_R_TAG:
    /* keep left channel, throw away right */
    get += 1;				/* start at right channel */
    if (samplingRate==SRATE_22K_TAG) {
      /* Mono Right, 22K: decimate by 2, only keep the Right channel. */
      write_samples /= 4;
      for (i=0;i<write_samples;i++) {
	*put++ = *get;
	get += 4;
      }
    } else {
      /* Mono Right, 44K: no decimation, only keep the right channel. */
      write_samples /= 2;
      for (i=0;i<write_samples;i++) {
	*put++ = *get;
	get += 2;
      }
    }
    break;
  }

  i = write(outFileFD, data, write_samples*sizeof(short));
  if (i < 0) {
    NXRunAlertPanel(NULL,"Failed to write output data","OK",NULL,NULL);
    [dspRecorder stop]; 
    return nil;
  }

}

@end

