// The ScorePlayer object handles reading scorefiles as
// well as starting and stopping playback.  Playback is
// done in a separate thread.  Errors are ignored.

// This code is a highly modified version of the code
// used in the ScorePlayer.app music kit example.
// I have added several methods and removed most of the
// error messages, since the errors are not useful in a game.

// Note: if you plan to use sounds simultaneously with the
// music, you CANNOT use the NeXT Sound object's -play
// method!!!  You have to allocate and set up your own
// SoundOut and PlayStream objects and go through them.
// You can use the Sound object to store/convert the data,
// but not play it.  See the SoundPlayer.[hm] files in this
// distribution to see how to accomplish this.

// This should be the default score to be loaded:
#define defaultFileName "Default.score"

#import "ScorePlayer.h"
#import <appkit/appkit.h>
#import <objc/NXBundle.h>
#import <musickit/musickit.h>
#import <string.h>
#import <libc.h>
#import <mach/cthreads.h>
#import <mach/mach.h>
#import <mach/mach_error.h>
#import	<mach/message.h>
#import <objc/objc-runtime.h>

@implementation ScorePlayer

// Strings used in alert panel.  Ought to be localized eventually.
#define OBJECTNAME "Load Score"
#define CANTLOAD "Unable to load music score file."
#define OK "OK"

static BOOL playScoreForm;
static id synthInstruments;
static id openPanel;
static char* fileName;
static id scoreObj,scorePerformer,theOrch;
static double samplingRate = 22050;
static double headroom = .1;
static BOOL userCancelFileRead = NO;
static double initialTempo = 60.0;
static double lastTempo = 60.0;
static double desiredTempo = 60.0;
static char *fileSuffixes[3] = {"score","playscore",NULL};
static id condClass = nil;
static id midis[2] = {0};
static int midiOffset;
static BOOL errorDuringPlayback = NO;
static BOOL firstPlay = YES;

#define PLAYING ([condClass performanceThread] != NO_CTHREAD)

#define SOUND_OUT_PAUSE_BUG 1	// Workaround for problem synching MIDI to DSP

static int handleObjcError(const char *className)
{ // ignore objc errors (like missing synthpatch classes)
    return 0;
}

static void handleMKError(char *msg)
{ // ignore all errors
	if (!PLAYING) {	// if can't read file (ie. parse error), cancel read
		userCancelFileRead = YES;
	}
}

void cantLoad()
{
	NXRunAlertPanel(OBJECTNAME, CANTLOAD, OK, NULL, NULL);
}

- _loadFile
{ // actually loads in the scorefile
    id tuningSys;
    id scoreInfo; 
	haveScore = NO; firstPlay = YES;                                  
    MKSetScorefileParseErrorAbort(10);
    if ((!fileName) || (!strlen(fileName))) { /* Can this ever happen? */ 
		return nil;
    }
    playScoreForm = (strstr(fileName,".playscore") != NULL);
    [scoreObj free];
    scoreObj = [Score new];
    userCancelFileRead = NO;
    tuningSys = [[TuningSystem alloc] init]; /* 12-tone equal tempered */
    [tuningSys install];
    [tuningSys free];
    if (![scoreObj readScorefile:(char *)fileName] || userCancelFileRead) {  
		cantLoad();
		scoreObj = [scoreObj free];
		fileName[0] = '\0';
		return nil;
	}
    samplingRate = 22050;
    headroom = .1;
    initialTempo = 60.0;
    [[condClass defaultConductor] setTempo:initialTempo];
    scoreInfo = [(Score *)scoreObj info];
    if (scoreInfo) { /* Configure performance as specified in info. */ 
	int midiOffsetPar;
	midiOffset = 0;
	midiOffsetPar = [Note parName:"midiOffset"];
	if ([scoreInfo isParPresent:midiOffsetPar])
		midiOffset = [scoreInfo parAsDouble:midiOffsetPar];
	if ([scoreInfo isParPresent:MK_headroom])
		headroom = [scoreInfo parAsDouble:MK_headroom];	  
	if ([scoreInfo isParPresent:MK_samplingRate]) {
	    samplingRate = [scoreInfo parAsDouble:MK_samplingRate];
	    if (!((samplingRate == 44100.0) || (samplingRate == 22050.0))) {
			samplingRate = 22050; // has to be one or the other!
	    }
	}
	if ([scoreInfo isParPresent:MK_tempo]) {
	    initialTempo = [scoreInfo parAsDouble:MK_tempo];
	    [[condClass defaultConductor] setTempo:initialTempo];
	} 
        #if SOUND_OUT_PAUSE_BUG
	if (samplingRate == 22050)
	    midiOffset +=  .36363636363636/8.0;
	else midiOffset += .181818181818181/8.0;
        #else
	if (samplingRate == 22050)
	    midiOffset +=  .36363636363636;
	else midiOffset += .181818181818181;
        #endif
	/* Note: there is a .1 second indeterminacy (in the 22khz case) due 
	   to not knowing where we are in soundout buffering. Using more, 
	   but smaller buffers would solve this. */
    } 
    lastTempo = desiredTempo = initialTempo;
	haveScore = YES;
    return self;
}

static port_t endOfTimePort = PORT_NULL;

-endOfTime	// called by the musickit thread
{	// when a performance completes
//    int i;
    msg_header_t msg =    {0,                   /* msg_unused */
                           TRUE,                /* msg_simple */
			   sizeof(msg_header_t),/* msg_size */
			   MSG_TYPE_NORMAL,     /* msg_type */
			   0};                  /* Fills in remaining fields */
    [theOrch close]; /* This will block! */
//    for (i=0; i<2; i++) {
//	[midis[i] close];
//	midis[i] = nil;
//    }
    [theOrch setSoundOut:YES];
    msg.msg_local_port = PORT_NULL;
    msg.msg_remote_port = endOfTimePort;
    msg_send(&msg, SEND_TIMEOUT, 0);
    return self;
}

void *endOfTimeProc(msg_header_t *msg,ScorePlayer *myself )
{
	// Tell delegate that the score finished.
	[myself scoreFinishedPlaying];
    return myself;
}

static BOOL isMidiClassName(char *className)
{
    return (className && ((strcmp(className,"midi") == 0)  ||
			  (strcmp(className,"midi1") == 0) ||
			  (strcmp(className,"midi0") == 0)));
}

#if SOUND_OUT_PAUSE_BUG

static BOOL checkForMidi(Score *obj)
{
    id subobjs;
    int i,cnt;
    id info;
    subobjs = [obj parts];
    if (!subobjs)
      return NO;
    cnt = [subobjs count];
    for (i=0; i<cnt; i++) {
	info = [(Part *)[subobjs objectAt:i] info];
	if ([info isParPresent:MK_synthPatch] &&
	    (isMidiClassName([info parAsStringNoCopy:MK_synthPatch]))) {
	    [subobjs free];
	    return YES;
	}
    }
    [subobjs free];
    return NO;
}
#endif

- _playIt
{ // initiate playback in separate MK thread
    int partCount,synthPatchCount,voices,i,whichMidi,midiChan;
    char *className;
    id partPerformers,synthPatchClass,partPerformer,partInfo,anIns,aPart;

// if (firstPlay) {   /* Could keep these around, in repeat-play cases: */ 
//    scorePerformer = [scorePerformer free];
//    [synthInstruments freeObjects];
//    synthInstruments = [synthInstruments free];
//}
    theOrch = [Orchestra newOnDSP:0]; /* A noop if it exists */
    [theOrch setHeadroom:headroom];    /* Must be reset for each play */ 
    [theOrch setSamplingRate:samplingRate];
#if SOUND_OUT_PAUSE_BUG
    if (checkForMidi(scoreObj))
	[theOrch setFastResponse:YES];
    else [theOrch setFastResponse:NO];
#endif
    [theOrch setOutputCommandsFile:NULL];
    [theOrch setOutputSoundfile:NULL];
    [theOrch setSoundOut:YES];
    if (![theOrch open]) {	// can't get DSP, so abort
		return nil;
    }
//if (firstPlay) {
    scorePerformer = [ScorePerformer new];
    [scorePerformer setScore:scoreObj];
    [(ScorePerformer *)scorePerformer activate]; 
    partPerformers = [scorePerformer partPerformers];
    partCount = [partPerformers count];
    synthInstruments = [List new];
    for (i = 0; i < partCount; i++) {
	partPerformer = [partPerformers objectAt:i];
	aPart = [partPerformer part]; 
	partInfo = [(Part *)aPart info];      
	if ((!partInfo) || ![partInfo isParPresent:MK_synthPatch]) {
	    continue; // missing parm.  Just ignore.
	}		
	className = [partInfo parAsStringNoCopy:MK_synthPatch];
	if (isMidiClassName(className)) {
	    midiChan = [partInfo parAsInt:MK_midiChan];
	    if ((midiChan == MAXINT) || (midiChan > 16))
		midiChan = 1;
	    if (strcmp(className,"midi") == 0)
		className = "midi1";
	    if (strcmp(className,"midi1") == 0) 
		whichMidi = 1;
	    else whichMidi = 0;
	    if (midis[whichMidi] == nil)
		midis[whichMidi] = [Midi newOnDevice:className];
	    [[partPerformer noteSender] connect:
	     [midis[whichMidi] channelNoteReceiver:midiChan]];
	} else {
	    synthPatchClass = (strlen(className) ? 
			       [SynthPatch findSynthPatchClass:className] : nil);
	    if (!synthPatchClass) {         /* Class not loaded in program? */
			haveScore = NO;
			cantLoad();
		    return nil;
		/* We would prefer to do dynamic loading here. */
	    }
	    anIns = [SynthInstrument new];      
	    [synthInstruments addObject:anIns];
	    [[partPerformer noteSender] connect:[anIns noteReceiver]];
	    [anIns setSynthPatchClass:synthPatchClass];
	    if (![partInfo isParPresent:MK_synthPatchCount])
		continue;         
	    voices = [partInfo parAsInt:MK_synthPatchCount];
	    synthPatchCount = 
		[anIns setSynthPatchCount:voices patchTemplate:
		 [synthPatchClass patchTemplateFor:partInfo]];
	    if (synthPatchCount < voices) { // ignore problem
	    }
	}
    }
//    [partPerformers free];
//}
    errorDuringPlayback = NO;
    MKSetDeltaT(.75);
    [Orchestra setTimed:YES];
    [condClass afterPerformanceSel:@selector(endOfTime) to:self argCount:0];
    for (i=0; i<2; i++) 
		[midis[i] openOutputOnly]; /* midis[i] is nil if not in use */
    for (i=0; i<2; i++) 
		if (midiOffset > 0) 
			[midis[i] setLocalDeltaT:midiOffset];
		else if (midiOffset < 0)
			[theOrch setLocalDeltaT:-midiOffset];
    for (i=0; i<2; i++) 
	[midis[i] run]; firstPlay = NO;
    [theOrch run];
    [condClass startPerformance];     
    return self;
}

extern void _MKSetConductorThreadMaxStress(int arg);

- init
{ // set up our object.  I really ought to change to using a +new
  // type of method since there should only ever be one ScorePlayer.
    static int inited = 0;
    int ec;
	[super init];
    if (inited++)
      return self;
	haveScore = NO;
    condClass = [Conductor class];
    [condClass setThreadPriority:1.0];
    setuid(getuid()); /* Must be after setThreadPriority. */
    [condClass useSeparateThread:YES];
    /* These numbers could be endlessly tweaked */
    MKSetLowDeltaTThreshold(.25);
    MKSetHighDeltaTThreshold(.4);
    _MKSetConductorThreadMaxStress(1000000); /* Don't do cthread_yields */
    ec = port_allocate(task_self(), &endOfTimePort);
    DPSAddPort(endOfTimePort,(DPSPortProc)endOfTimeProc,
	       sizeof(msg_header_t),(void *)self,30);
    MKSetErrorProc(handleMKError);
    objc_setClassHandler(handleObjcError);
    return self;
}

int setUpFile()
{ // use open panel to grab a score/playscore file.
    int success;
	char *shortFileName, *dir;
    static BOOL firstTime = YES;
    if (!openPanel)
        openPanel = [OpenPanel new];    
	if ((firstTime) && !fileName)
	  success = [openPanel 
		   runModalForDirectory:"/LocalLibrary/Music/Scores"
		   file:"Examp1.score" 
		   types:(const char *const *)fileSuffixes]; 
	else if (fileName) { // split into dir & name & run open panel
		dir = NXCopyStringBuffer((const char *)fileName);
		shortFileName = rindex(dir, '/') + 1;
		shortFileName[0] = '\0'; // isolate directory
		shortFileName = rindex(fileName, '/') + 1; // isolate filename
	    success = [openPanel 
		     runModalForDirectory:dir
		     file:shortFileName 
		     types:(const char *const *)fileSuffixes]; 
	    free(dir);
	} else success = [openPanel 
			runModalForTypes:(const char *const *)fileSuffixes];
	if (!success) return NO;
	fileName = NXCopyStringBuffer((const char *)[openPanel filename]);
	// save the choice.
    NXWriteDefault ([NXApp appName], "ScoreName", fileName);
    firstTime = NO;
    return YES;
}

- _abort
{ // abort (stop) a performance
    int i;
    if (PLAYING) {
	[condClass lockPerformance];
	for (i=0; i<2; i++) 
	  if (midis[i]) {
	      [midis[i] allNotesOff];
	      [midis[i] abort];
	  }
	[theOrch abort];
	[condClass finishPerformance];
	[condClass unlockPerformance];
	cthread_yield();
	while (PLAYING) ; /* Make sure it's really done. */
    }
	return self;
}


// loading a file always stops playback, but restarts playing after
// the new file is loaded if music was playing before the load.
// this is the most useful behavior for a game, IMHO...
// to change this, make a subclass that does something like this
// for all three score file loading methods:
//
// -loadfile { [self stop:self]; return [super loadFile]; }

- loadFile
{ // load default file in.
	BOOL wasPlaying = PLAYING; char *slashPos;
	const char *tmpstr = NXGetDefaultValue ([NXApp appName], "ScoreName");
	aborted = YES;
    if (PLAYING) [self _abort];
	if (fileName) free(fileName);
	if (!tmpstr) { // if no default yet, use built in score
		fileName = malloc(MAXPATHLEN);
		strcpy(fileName, NXArgv[0]);
		if (slashPos = strrchr(fileName, '/')) {
			slashPos[1] = '\0';
		} else {
			strcpy(fileName, "./");
		}
		strcat(fileName, defaultFileName);
	} else fileName = NXCopyStringBuffer(tmpstr);
	[self _loadFile];
	if (wasPlaying) [self play:self];
	return self;
}

- readScoreFile:(const char *)pathName;	// open scorefile (full pathname)
{ // get a scorefile.  give full path!
	BOOL wasPlaying = PLAYING;
	aborted = YES;
    if (PLAYING) [self _abort];
	strcpy(fileName, pathName);
	[self _loadFile];
	if (wasPlaying) [self play:self];
	return self;
}

- selectFile:sender
{ // get the scorefile to use
	BOOL wasPlaying = PLAYING;
	aborted = YES;
    if (PLAYING) [self _abort];
    if (!setUpFile(NULL)) {
      return self;
    }
    [self _loadFile];
	if (wasPlaying) [self play:self];
    return self;
}

- play:sender
{ // initiate a performance
    if ((!haveScore) || (!fileName) || (!strlen(fileName))) return nil;
    if (PLAYING) return self;
	aborted = NO;
	[self _playIt];
	return self;
}

- stop:sender
{ // stop a performance
	aborted = YES;
    if (PLAYING) [self _abort];
    return self;
}

// set up a delegate
- delegate { return delegate; }
- setDelegate:newDelegate
{
	id oldDelegate = delegate;
	delegate = newDelegate;
	return oldDelegate;
}

// delegate can implement this to be notified when a score
// finishes playing.  If no delegate, default implementation
// is to start playing the score again.
- scoreFinishedPlaying
{
	if (delegate) {
		if ([delegate respondsTo:@selector(scoreFinishedPlaying)])
			return [delegate scoreFinishedPlaying];
	} else {	// restart unless we were sent a -stop: message
		if (!aborted) return [self play:self];
	}
	return self; // never actually get here but suppresses a warning
}

@end

