This is ScorePlayer.m in view mode; [Download] [Up]
// 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
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.