home *** CD-ROM | disk | FTP | other *** search
- // 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 <gamekit/gamekit.h>
- #import <daymisckit/daymisckit.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;
- }
-
- - appDidInit:sender // forwarded by GameBrain -- just loads score
- {
- [self loadFile];
- 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
-
-