--Author: John Maxwell --last modified: December 18, 1981 8:47 AM DIRECTORY ImageDefs USING [AddCleanupProcedure, CleanupItem, CleanupProcedure, CleanupMask], InlineDefs USING [BITSHIFT,HighHalf,LowHalf], IODefs USING [WriteLine,WriteNumber,WriteString], Mopcodes USING [zMISC], MusicDefs, MusicHardwareDefs, Note USING [Duration,GetBackTie], Piece USING [AddSync,NearestSync], ProcessDefs USING [CV, DisableInterrupts, EnableInterrupts, GetPriority, InterruptLevel, Priority, SetPriority, SetTimeout], Real USING [Fix], Sync USING [AddNote], SystemDefs USING [AllocateSegment], Score USING [AddToPitch,GetKey,GetMetrenome], Screen USING [DisplayMessage,InvertListen,InvertPlay], Utility USING [NewNote,NewSync], Voice USING [ClearState,SetState,State,StatePTR]; MusicProcess: MONITOR IMPORTS ImageDefs, InlineDefs, IODefs, MusicDefs, Note, Piece, ProcessDefs, Real, Sync, SystemDefs, Score, Screen, Utility, Voice EXPORTS MusicDefs, Score = BEGIN OPEN MusicDefs, MusicHardwareDefs; listen:CONDITION; -- microcode signals whenever a buffer is ready for listenProcess play:CONDITION; -- microcode signals whenever it is through with a bufer listenProcess,playProcess: PROCESS; scoreIN: PiecePTR _ NIL; scoreOUT: PiecePTR _ NIL; playPhysical:BOOLEAN _ FALSE; offset:LONG CARDINAL; -- since we can't set musicIO.time, we use an offset to start in the middle of a piece. meter:INTEGER; -- speed of play (halftime, normal, doubletime). NOT metrenome! --****************************************************************** --code necessary to initialize the processes --****************************************************************** cleanup:ImageDefs.CleanupItem; InitializeSynthesizer:PUBLIC PROCEDURE = BEGIN originalLevel: ProcessDefs.Priority _ ProcessDefs.GetPriority[]; cleanup_[NIL,ImageDefs.CleanupMask[Finish] +ImageDefs.CleanupMask[Save] +ImageDefs.CleanupMask[Restore] +ImageDefs.CleanupMask[Checkpoint] +ImageDefs.CleanupMask[Restart], CleanupMicrocode]; ImageDefs.AddCleanupProcedure[@cleanup]; InitMicrocode[]; ProcessDefs.SetTimeout[@play,1]; ProcessDefs.SetPriority[2]; listenProcess _ FORK ProcessBuffer; playProcess _ FORK ProcessScore; ProcessDefs.SetPriority[originalLevel]; END; StartListening:PUBLIC PROCEDURE[s:PiecePTR]= BEGIN IF playing THEN RETURN; meter_128; sync _ NIL; offset _0; IF debugInput THEN IODefs.WriteLine["offset reset--input"]; counter _ 0; scoreIN _ s; setOffset_TRUE; partial _ ALL[[pressed,0,0]]; keyboardIN _ ALL[[0,[up,up,up,up,up,up]]]; StartMicrocode[listening]; END; StopListening:PUBLIC ENTRY PROCEDURE = BEGIN StopMicrocode[listening]; NOTIFY listen; END; StartPlaying:PUBLIC ENTRY PROCEDURE[s:PiecePTR,first:CARDINAL,physical:BOOLEAN, displayCursor:PROCEDURE[time:LONG CARDINAL]]= BEGIN IF listening AND (scoreIN[0]#NIL OR ~Empty[INbuffer]) THEN RETURN; IF ~physical THEN StopMicrocode[listening]; pendingArray _ ALL[[NIL,NIL,0,up,FALSE,0]]; pending _ NIL; scoreOUT _ s; in _ 0; -- in and out are used by the cursor displayer out _ 1; start _ first; setOffset_TRUE; SELECT TRUE FROM BlueBug[] => meter _ 256; RedBug[] => meter _ 64; ENDCASE => meter _ 128; playPhysical _ physical; DisplayCursor _ displayCursor; StartMicrocode[playing]; IF ~microcode THEN RealStopMicrocode[]; NOTIFY play; --you fool! you forgot to loadmb music.mb! END; StopPlaying:PUBLIC PROCEDURE = BEGIN StopMicrocode[playing]; IF listening AND scoreIN[0]=NIL AND Empty[INbuffer] THEN StopListening[]; END; Empty:PROCEDURE[b:EventBufferPTR] RETURNS[BOOLEAN] = BEGIN IF b=NIL THEN RETURN[TRUE]; FOR i:CARDINAL IN [0..b.length) DO IF b.data[i].htoc<100 THEN LOOP; -- transient garbage RETURN[FALSE]; ENDLOOP; RETURN[TRUE]; END; DisplayCursor:PROCEDURE[time:LONG CARDINAL]; --****************************************************************** --this submodule is responsible for translating from events to notes --****************************************************************** --the microcode passes us a buffer of keystates and their times. --we must compare successive keystates to see where changes occur. --the keystates may include spurious "bounce". --channel=15 means that the microcode's clock wrapped around (it's only 16 bits). setOffset:BOOLEAN _ FALSE; -- so we know when to determine the offset counter:CARDINAL; -- number of times the microcode clock has wrapped around ProcessBuffer: ENTRY PROCEDURE = BEGIN c,channel:CARDINAL _ 0; compare:ChannelWord; event:MusicEvent; DO --wait until a buffer is ready and then process it WHILE ~listening AND INbuffer.length=0 DO WAIT listen; ENDLOOP; WHILE INbuffer = musicIO.headOfInputList DO WAIT listen; ENDLOOP; IF ~microcode THEN RealStopMicrocode[]; IF debugInput THEN WriteBuffer[INbuffer]; FOR c IN [0..INbuffer.length) DO INbuffer.data[c].channelWord.channel _ --! hack to work around bug in the microcode SELECT INbuffer.data[c].channelWord.channel FROM 12 => 0, 15 => 15, ENDCASE => INbuffer.data[c].channelWord.channel + 1; event _ INbuffer.data[c]; channel _ event.channelWord.channel; IF channel = 15 THEN counter _ counter + 1; IF channel > 12 THEN LOOP; IF counter=0 AND event.htoc < 100 THEN LOOP; --transient garbage compare _ keyboardIN[channel]; ExtractTransitions[compare,event.channelWord,event.htoc]; keyboardIN[channel] _ event.channelWord; ENDLOOP; IF ~listening THEN FOR i:CARDINAL IN [0..12] DO IF keyboardIN[i].key = [up,up,up,up,up,up] THEN LOOP; compare _ keyboardIN[i]; event.channelWord _ [i,[up,up,up,up,up,up]]; event.htoc _ MAX[event.htoc,InlineDefs.LowHalf[musicIO.time]]; ExtractTransitions[compare,event.channelWord,event.htoc]; keyboardIN[channel] _ event.channelWord; ENDLOOP; GetNextInputBuffer[]; ENDLOOP; END; ExtractTransitions:INTERNAL PROCEDURE[x,y:ChannelWord,time:CARDINAL]= BEGIN OPEN InlineDefs; delta,pitch:INTEGER; channel:CARDINAL=y.channel; transition:RECORD[a,b,c:KeyState]; e:MusicEvent _ [LOOPHOLE[time],counter]; toc:LONG CARDINAL _ LOOPHOLE[e]; toc _ Scale[toc,meter,128]+offset; IF setOffset THEN {setOffset _ FALSE; offset _ 500-toc; toc _ 500; IF debugInput THEN {WriteLong[offset]; IODefs.WriteLine[" (offset}"]}}; -- each channel has 6 keys, and there might be more than one transition FOR i:CARDINAL IN [0..6) DO IF x.key[i]=y.key[i] THEN LOOP; pitch _ Pitch[channel,i]; delta _ InlineDefs.LowHalf[(toc-partial[pitch].toc)]; IF delta<0 THEN delta _ 32000; transition _ [partial[pitch].last,x.key[i],y.key[i]]; SELECT transition FROM [pressed,up,pressed] => partial[pitch].toc_ toc; [up,pressed,up] => partial[pitch].toc_ 0; -- ignore transient bounce [up,pressed,down] => NULL; --partial[pitch].loudness_ delta; [down,pressed,down] => NULL; [pressed,down,pressed] => partial[pitch].duration_ delta; [pressed,down,up] => {partial[pitch].duration_ delta; AddNote[pitch]}; [down,pressed,up] => AddNote[pitch]; ENDCASE => Error[]; partial[pitch].last _ x.key[i]; ENDLOOP; END; AddNote:INTERNAL PROCEDURE[pitch:CARDINAL] = BEGIN note:NotePTR; IF partial[pitch].toc=0 THEN RETURN; IF partial[pitch].duration=0 THEN Error[]; note _ Utility.NewNote[]; note^ _ []; --defaults to the values listed in MusicDefs note.pitch _ pitch; note.toc _ partial[pitch].toc; note.duration _ partial[pitch].duration; --note.loudness _ partial[pitch].loudness; SELECT TRUE FROM note.pitch IN [79..92] => BEGIN note.stemUp_FALSE; note.staff_0; END; note.pitch IN [68..78] => BEGIN note.stemUp_ TRUE; note.staff_0; END; note.pitch IN [55..67] => BEGIN note.stemUp_FALSE; note.staff_1; END; note.pitch IN [44..54] => BEGIN note.stemUp_ TRUE; note.staff_1; END; note.pitch IN [34..43] => BEGIN note.stemUp_FALSE; note.staff_2; END; note.pitch IN [22..33] => BEGIN note.stemUp_ TRUE; note.staff_2; END; note.pitch IN [12..21] => BEGIN note.stemUp_FALSE; note.staff_3; END; note.pitch IN [5..11] => BEGIN note.stemUp_ TRUE; note.staff_3; END; ENDCASE; IF sync=NIL OR ABS[note.toc - sync.time*MusicDefs.TF] > 200 THEN sync _ scoreIN[Piece.NearestSync[scoreIN,note.toc/MusicDefs.TF,TRUE]]; IF sync=NIL OR ABS[note.toc - sync.time*MusicDefs.TF] > 200 THEN BEGIN sync _ Utility.NewSync[]; sync.type _ notes; sync.time _ note.toc/MusicDefs.TF; Piece.AddSync[scoreIN,sync]; END; Sync.AddNote[sync,note]; partial[pitch].toc _ 0; partial[pitch].duration _ 0; END; sync:SyncPTR_NIL; Pitch:PROCEDURE[channel,note:CARDINAL] RETURNS[CARDINAL]= INLINE BEGIN RETURN[channel*6 + note+12]; END; partial:ARRAY [0..100) OF RECORD[last:KeyState,toc:LONG CARDINAL,duration:CARDINAL--,loudness:CARDINAL--]; keyboardIN:ARRAY [0..12] OF ChannelWord; --****************************************************************** --this submodule is responsible for translating from notes to events --****************************************************************** start:CARDINAL_0; ProcessScore:PROCEDURE = BEGIN endOfTime:LONG CARDINAL _ 0; endOfTime _ endOfTime-1; -- wraps around to largest long cardinal trill _InlineDefs.LowHalf[Scale[512,128,70]]; trillDur _(3*trill)/4; grace _InlineDefs.LowHalf[Scale[300,128,70]]; graceDur _(3*grace)/2; DO -- forever WHILE NOT playing DO WaitOnPlay[]; ENDLOOP; musicIO.time _ 0; OUTbuffer.data[0] _ reStart; OUTbuffer.length _ 1; lastTOC _ 0; InitKeyboardOUT[]; IF debugOutput THEN IODefs.WriteLine["processing score"]; IF playPhysical THEN TranslatePhysical[] ELSE TranslateLogical[]; --done with the main score, clean out pending IF debugOutput THEN IODefs.WriteLine["pending array"]; FlushPending[endOfTime]; -- flushes everything GetNextOutputBuffer[]; -- has side effect of writing current buffer WHILE playing DO -- wait for microcode to finish IF musicIO.headOfOutputList=NIL THEN StopPlaying[] ELSE WaitOnPlay[]; ENDLOOP; ENDLOOP; END; TranslatePhysical:PROCEDURE = BEGIN n:NotePTR; sync:SyncPTR; time,maxTime,lastFlush,duration:LONG CARDINAL_0; FOR i:CARDINAL IN [start..maxScoreLength) DO IF NOT playing THEN EXIT; IF (sync_scoreOUT[i])=NIL THEN EXIT; FOR j:CARDINAL IN [0..syncLength) DO IF (n_sync.event[j])=NIL THEN EXIT; IF n.rest THEN LOOP; IF n.tie#NIL AND n.tie.pitch=n.pitch THEN LOOP; -- skip the second note of a tied pair IF voice AND n.voice#selectedVoice THEN LOOP; IF n.toc=0 THEN time _ n.sync.time*TF ELSE time _ n.toc; IF n.duration=0 THEN duration _ Duration[n,128] ELSE duration _ n.duration; maxTime _ MAX[maxTime,time]; ProcessNote[MAX[time,lastFlush],duration,n.pitch,loudness] ENDLOOP; IF maxTime>1000 THEN lastFlush _ maxTime-1000; FlushPending[lastFlush]; ENDLOOP; END; TranslateLogical:PROCEDURE = BEGIN i,j:CARDINAL; n:NotePTR_NIL; vs:Voice.State; sync:SyncPTR_NIL; now,delta,toc,max:Time_0; lastDuration,lastNow,lastToc:Time_0; pitch,metrenome,count:INTEGER _ 0; isLogicalSync,wasLogicalSync,nonGraceSync:BOOLEAN_TRUE; metrenome _ Score.GetMetrenome[scoreOUT[start].time]; index _ [0,0]; Voice.ClearState[@vs]; FOR i:CARDINAL IN [0..maxVoice] DO vs[i].sum_10*trill; ENDLOOP; FOR i IN [start..maxScoreLength) DO IF NOT playing THEN EXIT; IF (sync_score[i])=NIL THEN LOOP; -- handle all of the syncs that don't contain notes IF sync.type=repeat1 THEN PushRepeat[i,stack1]; -- encountered a left-paren repeat IF sync.type=repeat2 THEN BEGIN -- encountered a right-paren repeat IF TopRepeat[stack2]=i THEN -- we've repeated once, don't repeat again. BEGIN []_PopRepeat[stack2]; LOOP; END; IF NoRepeat[stack1] THEN LOOP; PushRepeat[i,stack2]; -- save repeat so we won't repeat twice i _ PopRepeat[stack1]; -- go to beginning of the repeat END; IF sync.type=metrenome THEN metrenome_sync.value; IF sync.type#notes THEN LOOP; -- handle syncs that contain notes -- is this a logical sync? what is its toc? isLogicalSync _ FALSE; toc _ 0; nonGraceSync _ FALSE; FOR j IN [0..syncLength) DO IF (n_sync.event[j])=NIL THEN EXIT; toc _ MAX[toc, sync.event[j].toc]; IF ~n.grace THEN nonGraceSync _ TRUE; IF sync.event[j].value#unknown THEN isLogicalSync _ TRUE; ENDLOOP; --determine the time to play for the whole sync. --handle boundarys between physical and logical sections max _ Voice.SetState[@vs,sync,metrenome]; IF wasLogicalSync AND isLogicalSync THEN IF max > now THEN now _ max ELSE now _ lastNow+lastDuration; IF wasLogicalSync AND ~isLogicalSync THEN { IF lastToc#0 THEN delta _ lastNow-lastToc ELSE delta_ (lastNow+lastDuration)-toc; now_ toc+delta}; IF ~wasLogicalSync AND ~isLogicalSync THEN now _ toc+delta; IF ~wasLogicalSync AND isLogicalSync THEN IF toc#0 AND toc+delta>now AND toc+delta BEGIN -- n.tie is tied back to a trill time:LONG CARDINAL _ toc; duration _ Note.Duration[n,metrenome]; ProcessNote[time,trillDur,n.pitch,loudness]; ProcessNote[time+trill,trillDur,Score.AddToPitch[key,n.pitch,1],loudness]; time _ time+2*trill; duration _ duration-2*trill; WHILE duration>=2*trill DO ProcessNote[time,trillDur,n.pitch,loudness]; ProcessNote[time+trill,trillDur,Score.AddToPitch[key,n.pitch,1],loudness]; time _ time+2*trill; duration _ duration-2*trill; ENDLOOP; END; n.embellish=mordent1 => BEGIN ProcessNote[toc-2*trill,trillDur,n.pitch,loudness]; ProcessNote[toc-trill,trillDur,Score.AddToPitch[key,n.pitch,1],loudness]; ProcessNote[toc,duration,n.pitch,loudness]; END; n.embellish=mordent2 => BEGIN ProcessNote[toc-2*trill,trillDur,n.pitch,loudness]; ProcessNote[toc-trill,trillDur,Score.AddToPitch[key,n.pitch,-1],loudness]; ProcessNote[toc,duration,n.pitch,loudness]; END; n.grace => ProcessNote[toc,graceDur,n.pitch,loudness]; ENDCASE => ProcessNote[toc,duration,n.pitch,loudness]; END; Trilled:PROCEDURE[n:NotePTR] RETURNS[BOOLEAN] = BEGIN WHILE n.tie#NIL DO IF n.tie.pitch#n.pitch THEN RETURN[FALSE]; IF (n_n.tie).embellish=trill THEN RETURN[TRUE]; IF n.embellish#none THEN RETURN[FALSE]; ENDLOOP; RETURN[FALSE]; END; Duration:PROCEDURE[n:NotePTR,metrenome:INTEGER] RETURNS[INTEGER] = BEGIN l:REAL_1; tie:NotePTR; d:Time _ Note.Duration[n,metrenome]; IF n.grace THEN d_grace; WHILE n.tied DO IF n.embellish=trill THEN EXIT; tie _ Note.GetBackTie[n]; IF n.pitch#tie.pitch THEN EXIT; d _ d + Note.Duration[tie,metrenome]; n _ tie; ENDLOOP; RETURN[InlineDefs.LowHalf[MIN[7*(d/8),32000]]]; END; Scale:PROCEDURE[time,top,bottom:LONG CARDINAL] RETURNS[LONG CARDINAL] = BEGIN l:REAL _ 1; RETURN[Real.Fix[l*top*time/bottom]]; END; loudness:INTEGER _ 90; --****************************************************************** --procedures needed to handle repeats correctly --****************************************************************** PushRepeat:PROCEDURE[i,s:CARDINAL] = BEGIN stack[s][index[s]] _ i; index[s] _ index[s] + 1; END; PopRepeat:PROCEDURE[s:CARDINAL] RETURNS[CARDINAL] = BEGIN IF NoRepeat[s] THEN Error; index[s] _ index[s] - 1; RETURN[stack[s][index[s]]]; END; TopRepeat:PROCEDURE[s:CARDINAL] RETURNS[CARDINAL] = BEGIN RETURN[IF NoRepeat[s] THEN 10000 ELSE stack[s][index[s]-1]]; END; NoRepeat:PROCEDURE[s:CARDINAL] RETURNS[BOOLEAN] = BEGIN RETURN[index[s]=0] END; stack:ARRAY [1..2] OF ARRAY [0..10] OF CARDINAL; index:ARRAY [1..2] OF CARDINAL _ [0,0]; stack1:CARDINAL = 1; stack2:CARDINAL = 2; --****************************************************************** -- Waiting and displaying cursor --****************************************************************** --We want to display the cursor near the note that the microcode is playing, not the one we are processing. --All we know about the microcode is its current time. (musicIO.time) --We save the actual times we said to play syncs long enough to guess what sync is being played. WaitOnPlay:ENTRY PROCEDURE = BEGIN time:Time; sync:SyncPTR; WAIT play; WHILE dataStructureInFlux DO WAIT play; ENDLOOP; IF musicIO.headOfOutputList=NIL THEN RETURN; time _ Scale[musicIO.time,meter,128]+offset; IF playPhysical THEN DisplayCursor[time] ELSE IF (sync_GetSync[time])#NIL THEN DisplayCursor[sync.time]; END; Associate:PROCEDURE[time:LONG CARDINAL,sync:CARDINAL] = BEGIN IF list[in] = [time,sync] THEN RETURN; in _ (in+1) MOD listLength; list[in] _ [time,sync]; END; GetSync:PROCEDURE[time:LONG CARDINAL] RETURNS[SyncPTR] = BEGIN IF out = (in+1) MOD listLength THEN RETURN[NIL]; WHILE list[(out+1) MOD listLength].time87 DO pitch_pitch-12; ENDLOOP; -- ditto IF pitch>72 THEN loudness_(2*loudness)/3; -- adjust for dynamics of keyboard IF setOffset THEN {offset_(IF time>500 THEN time-500 ELSE 0); setOffset_FALSE; IF debugOutput THEN {WriteLong[offset]; IODefs.WriteLine[" (offset)"]}}; time _ Scale[(time-offset),128,meter]; -- adjust for tempo (not metrenome!) duration _ Scale[duration,128,meter]; duration _ MAX[duration,2*loudness]; time_MAX[time,lastTOC]; FOR temp _ pending, temp.next WHILE temp#NIL DO --find out if there is another note pending at this pitch and time IF temp.pitch#pitch THEN LOOP; IF temp.toc