/*
 * SoundGenerator.m
 * Author: Ali Ozer, with assistance from David Jaffe and Doug Keislar.
 * Written for 1.0 5-May-89.
 * Last updated for 1.0 on 15-Aug-89.
 * Updated for 2.0 on 5-Sep-90; separated the panel into its own .nib file.
 * Updated for 2.0 on 9-Oct-90 by David Jaffe and Ali Ozer: changed Music Kit 
 *	code to be multi-threaded and timed, and made various other changes. 
 *
 * This class allows you to generate notes through the MusicKit; create an 
 * instance of this class, enable it, then use playNote: to play notes.
 * Currently the code in here isn't too general; you're stuck with no
 * more than NUMNOTES notes simultaneously, for instance, and not all
 * parameters can be changed at runtime.
 * 
 * SoundGenerator provides various IB-style target-action methods and
 * outlets to allow hooking up a user interface.  If you do hook up input
 * devices, pay attention to the various ranges of values allowed (see
 * comments below). Also note that the modulator index and frequency
 * multiplier input devices should be on a log scale; thus the values
 * used are actually equal to 10.0^(input value). 
 *
 *  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.
 */

// These values should be the same as those in the nib file
#define DEFAULTATTACK 0.14  // In seconds; a good range is 0.005..1
#define MINATTACK 0.005	    // Never below this with the current setup
#define DEFAULTDECAY 3    // In seconds; 0.01 to 10 or 20
#define MINDECAY 0.01	    // Some minimum value for decay...
#define DEFAULTRATIO 1.0    // Should range from 0 on up 
#define DEFAULTINDEX 0.1    // 0 to 20, perhaps
#define DEFAULTAMP 0.2	    // 0 to 1
#define DEFAULTTRANSPOSE 0     // -12 to 12, perhaps

#import "SoundGenerator.h"

#import <musickit/musickit.h>
#import <musickit/synthpatches/Fm1.h>
#import <musickit/params.h>
#import <math.h>
#import <appkit/Application.h>
#import <appkit/Button.h>
#import <appkit/Panel.h>

@implementation SoundGenerator

// Call new or alloc/init to create the object. init does not grab the DSP;
// it simply allocates and initializes the notes and various other
// things. You need to send enable to claim the DSP.

- init
{
    int i;
    double yArray[] = {0,1,0};
    double xArray[] = {0,1,2};    

    [super init];

    // Create a very simple Envelope. The time and value scaling is arbitrary 
    //               here because they'll be rescaled by the Note parameters.
    //               The envelope has 1 attack segment.
    ampEnvelope = [[Envelope allocFromZone:[self zone]] init];
    [ampEnvelope setPointCount:3 
		 xArray:xArray    // Arrays are copied by the object
	 	 orSamplingPeriod:0.0
		 yArray:yArray
		 smoothingArray:NULL
	 	 orDefaultSmoothing:0.2];
    [ampEnvelope setStickPoint:1];// Stickpoint is 1 (numbering from 0)

    transpose = DEFAULTTRANSPOSE;

    // Create the notes with some default values
    for (i = 0; i < NUMNOTES; i++) {
        notes[i] = [[Note allocFromZone:[self zone]] init];
  	[notes[i] setPar:MK_amp toDouble:DEFAULTAMP];      // amp scaling
	[notes[i] setPar:MK_ampRel toDouble:DEFAULTDECAY]; // amp release time
	[notes[i] setPar:MK_ampAtt toDouble:DEFAULTATTACK];// amp attack time
	[notes[i] setDur:DEFAULTATTACK];                   // duration
        // The duration is set to the same value as the attack time
        //     so that the note proceeds immediately from the attack to the 
        //     release.  Keep in mind that duration (dur) is defined as
        //     the time from the start of the note to the release.
	[notes[i] setNoteTag:MKNoteTag()];             // NUMNOTES "phrases"
  	[notes[i] setPar:MK_ampEnv toEnvelope:ampEnvelope];        
  	[notes[i] setPar:MK_m1Ratio toDouble:DEFAULTRATIO];
  	[notes[i] setPar:MK_m1Ind1 toDouble:DEFAULTINDEX];
    }

    // Create a SynthInstrument to manage SynthPatches of class Fm1.
    instrument = [[SynthInstrument allocFromZone:[self zone]] init];
    [instrument setSynthPatchClass:[Fm1 class]];

    orchestra = [Orchestra new];
    [orchestra setFastResponse:YES];	// Request minimum sound buffering 
    [orchestra setTimed:YES];		// We'll queue events 100 ms early
    MKSetDeltaT(.1);                    // See comment below

    /*
	For fastest response, we could run the orchestra in "untimed
	mode", which means that DSP commands are executed as soon as
	they're received.  However, since envelopes (in the SynthPatch
	Fm1) are fed one breakpoint at a time to the DSP by the host,
	and since there is buffering in the sound driver, very small
	attacks can get lost.  If you want very short attacks, there
	are two ways to go:
    
	1) Set the orchestra to timed mode and include a "delta time" of .1 by 
	   calling MKSetDeltaT(.1), which gives the DSP a 100 ms cushion.
    
	2) Change the envelope to begin at 1 instead of 0 when the attack is 
	   small
    
	We use solution 1 above.
    */

    // Now set some aspects of the performance:
    // Don't quit when the Conductor's messages-to-send queue is empty.
    [Conductor setFinishWhenEmpty:NO];

    // Run performance in separate high-priority thread for independence 
    // between animation and music
    [Conductor useSeparateThread:YES];  
    [Conductor setThreadPriority:1.0];

    return self;
}    

// The enable method tries to claim the DSP; returns YES if 
// successful. Call disable to let go of the DSP so that other apps can use 
// it.
    
-(BOOL) enable
{
    if ([Conductor inPerformance]) return YES;	 // Already enabled

    if (![orchestra open]) {
	return NO;	// To indicate that we could not grab the DSP
    }

    currentNote = 0;     

    // Set the maximum number of notes to play at any one time. This actually
    //    allocates and loads the patches on the DSP. Hence, it must be
    //    done after the Orchestra is open.
    if ([instrument setSynthPatchCount:NUMNOTES] != NUMNOTES) {
	[orchestra close];
	return NO;
    }

    // Start the DSP running and start the performance in a separate thread
    [orchestra run];				
    [Conductor startPerformance]; 

    // Get settings from the sound parameters panel, if initialized.
    if (attackInput) {  /* Panel initialized? */
       [self changeAttack:attackInput];
       [self changeDecay:decayInput];
       [self changeRatio:ratioInput];
       [self changeIndex:indexInput];
       [self changeTranspose:transposeInput];
    }
    return YES;
}

- disable
{
    if (![Conductor inPerformance]) return self;

    [Conductor lockPerformance];    // Get Music Kit thread lock
    [Conductor finishPerformance];  // Finishes performance
    [orchestra abort];              // Closes Orchestra without waiting
    [Conductor unlockPerformance];  // Gives up Music Kit thread lock
  
    return self;
}

// playNoteAtFreq: will play a note at the specified frequency using the
// parameters set with the various change... methods below.

- playNoteAtFreq:(double)freq
{
    if (![Conductor inPerformance]) return self;
    
    // Update Conductor's notion of the current time; lock performance thread
    [Conductor lockPerformance];

    // Use the supplied frequency 
    [notes[currentNote] setPar:MK_freq toDouble:freq*pow((double)2,(double)(transpose/12.0))]; 

    // Send the note off
    [[instrument noteReceiver] receiveNote:notes[currentNote]]; 

    // Send any buffered DSP commands and unlock performance thread
    [Conductor unlockPerformance];

    currentNote = (currentNote + 1) % NUMNOTES;

    return self;
}

// The free method gets rid of the object for good.

- free
{
    [self disable];
    [ampEnvelope free];
    {int i; for (i = 0; i < NUMNOTES; i++) [notes[i] free];}
    return [super free];
}

// Invoke shutUp to stop all the notes from generating sound.

- shutUp
{
    int i;

    if (![Conductor inPerformance]) return self;

    [Conductor lockPerformance];     // Get lock 
    for (i = 0; i < NUMNOTES; i++) { 
	[notes[i] setPar:MK_amp toDouble:0.0];           // zero amplitude
        [[instrument noteReceiver] receiveNote:notes[i]];// send notes
	[notes[i] setPar:MK_amp toDouble:DEFAULTAMP];    // restore amplitude
    }
    [Conductor unlockPerformance];   // Give lock

    return self;
}

// changeDecayParameter: and changeAttackParameter: allow you to set
// the decay & attack times (in seconds).

- changeDecayParameter:(double)newDecay
{
    newDecay = MAX(newDecay,MINDECAY);    // Clip to reasonable value
    [self changeParameter:MK_ampRel toDouble:newDecay];
    return self;
}

- changeAttackParameter:(double)newAttack
{
    int noteCnt;
    newAttack = MAX(newAttack,MINATTACK);  // Clip to reasonable value
    [self changeParameter:MK_ampAtt toDouble:newAttack];
    for (noteCnt = 0; noteCnt < NUMNOTES; noteCnt++) {
	[notes[noteCnt] setDur:newAttack];
    }
    return self;
}

// The following method initializes an already allocated amplitude envelope
// with values. The envelope has a sharp attack and a decay determined by
// the decayTime parameter.


// The following method allows you to change a certain parameter for all
// the notes. Only "toDouble:" version is provided. Note that the changes
// do not take effect until the Notes are sent to the instrument. 

- changeParameter:(int)param toDouble:(double)value
{
    int i;
    [Conductor lockPerformance];
    for (i = 0; i < NUMNOTES; i++) {
	[notes[i] setPar:param toDouble:value];
    }
    [Conductor unlockPerformance];
    return self;
}

// Function to round floating point numbers to certain number of
// decimal digits. decDigits should be 0..4.

static double RoundTo (double num, int decDigits)
{
    static double pow10[] = {1, 10., 100, 1000., 10000.};
    return (rint(num * pow10[decDigits])) / pow10[decDigits];
}

// The following three methods allow IB-style target/action input devices
// (like sliders) to set the values of the three-settable parameters of the 
// sound generator object, m1 ratio, m1 index, and decay time.
// After the values are set, they are displayed on the (optional) outputs.

- changeRatio:sender
{
    double newValue = RoundTo([sender doubleValue], 1);
    [self changeParameter:MK_m1Ratio toDouble:newValue];
    [ratioOutput setDoubleValue:newValue];
    return self;
}

- changeIndex:sender
{
    double newValue = RoundTo(pow(10.0, [sender doubleValue]) - 1.0, 1);
    [self changeParameter:MK_m1Ind1 toDouble:newValue];
    [indexOutput setDoubleValue:newValue];
    return self;
}

- changeDecay:sender
{
    double newValue = RoundTo([sender doubleValue], 1);
    [self changeDecayParameter:newValue];
    [decayOutput setDoubleValue:newValue];
    return self;
}

- changeAttack:sender
{
    double newValue = RoundTo([sender doubleValue], 2);
    [self changeAttackParameter:newValue];
    [attackOutput setDoubleValue:newValue];
    return self;
}

- changeTranspose:sender
{
    transpose = [sender intValue];
    [transposeOutput setIntValue:transpose];
    return self;
}

- changeState:sender
{
    if ([sender state]) {
	if (![self enable]) {
	    NXRunAlertPanel (NULL, "Can't open the DSP.", "OK", NULL, NULL);
	    [sender setState:NO];
	}
    } else {
	[self disable];
    }
    return self;
}

- play:sender
{
    [self playNoteAtFreq:[sender doubleValue]];
    return self;
}

// Method to load .nib files for the sound settings panel.

- showSoundSettings:sender
{
    if (!soundSettingsPanel) {
	[NXApp loadNibSection:"SoundSettings.nib"
			owner:self
			withNames:NO
			fromZone:[self zone]];
    }
    [soundSettingsPanel makeKeyAndOrderFront:sender];
    return self;
}


@end
   

