--Author: John Maxwell
--last modified: December 14, 1981 12:17 PM

DIRECTORY
Graphics USING [Texture],
InlineDefs USING [LongMult],
MusicDefs,
Note USING [Duration],
Piece USING [AddSync,CleanUpSyncs],
Score USING [GetTimeSignature],
Sheet USING [HiLite],
Sync USING [AddNote],
Utility USING [NewSync],
Voice USING [ClearState,State,StatePTR];

VoiceImpl: PROGRAM
IMPORTS InlineDefs, MusicDefs, Note, Piece, Score, Sheet, Sync, Utility, Voice
EXPORTS MusicDefs, Voice =
BEGIN
OPEN MusicDefs, Voice;

Error:SIGNAL = CODE;

voice
:PUBLIC BOOLEAN ← FALSE;
selectedVoice:PUBLIC CARDINAL ←0;
maxVoice:PUBLIC CARDINAL ← 0;

Set:PUBLIC PROCEDURE[v:CARDINAL] =
BEGIN
n:NotePTR;
i,j:CARDINAL;
IF lineSelect
THEN FOR i IN [0..scoreLength) DO
IF score[i]=NIL THEN LOOP;
IF score[i].time < select1 OR score[i].time > select2 THEN LOOP;
FOR j IN [0..syncLength) DO
IF score[i].event[j]=NIL THEN EXIT;
IF voice AND score[i].event[j].voice#selectedVoice THEN LOOP;
score[i].event[j].voice ← v;
ENDLOOP;
ENDLOOP
ELSE FOR i IN [0..selectionLength) DO
IF (n←selection[i])=NIL THEN LOOP;
IF voice AND n.voice#selectedVoice THEN LOOP;
SetDirty[n.sync.time,n.sync.time];
n.voice ← v;
ENDLOOP;
IF lineSelect THEN SetDirty[select1,select2];
maxVoice ← MAX[v,maxVoice];
END;

Check
:PUBLIC PROCEDURE =
BEGIN
vs:State;
i:CARDINAL;
sum,sumTS:Time←0;
begin,end:CARDINAL←0;
FOR i IN [0..scoreLength) DO
IF score[i].type = timeSignature THEN
sumTS←InlineDefs.LongMult[score[i].ts.top*256,64/score[i].ts.bottom];
IF ~Measure[score[i].type] THEN LOOP;
begin ← end; end ← i;
IF begin=end THEN LOOP;
sum ← Sum[begin,end,voice,@vs];
IF sum=0 THEN LOOP;
IF ABS[sum-sumTS]>8 THEN Sheet.HiLite[lightgrey,score[begin].time,score[end].time];
ENDLOOP;
END;


lightgrey:Graphics.Texture = 004040B;

Sum:PROCEDURE[begin,end:CARDINAL,separate:BOOLEAN,ss:StatePTR] RETURNS[sum:Time] =
BEGIN
sum←0;
ClearState[ss];
FOR i:CARDINAL IN (begin..end) DO
IF score[i].type#notes THEN LOOP;
[] ←SetState[ss,score[i],128,separate];
ENDLOOP;
[] ←SetState[ss,score[end],128,separate];
FOR v:CARDINAL IN [0..maxVoice] DO
IF separate AND v#selectedVoice THEN LOOP;
sum ← MAX[sum,ss[v].sum+ss[v].duration];
ENDLOOP;
END;

--*****************************************************************
-- enumerating over voices
--*****************************************************************

SetState:PUBLIC PROCEDURE[vs:StatePTR,s:SyncPTR,m:INTEGER,separate:BOOLEAN] RETURNS[max:Time]=
BEGIN
--
sum contains the total up to but not including the last duration
--duration contains the duration of the last note found
--a grace note occurs logically just a little before the note it graces
--a string of grace notes is treated as one grace note

n:NotePTR;
i:CARDINAL←0;
duration,offset:Time;
FOR i IN [0..10) DO vs[i].found ← vs[i].graced ← FALSE; ENDLOOP;
FOR i IN [0..syncLength) DO
IF (n←s.event[i])=NIL THEN EXIT;
IF n.value=unknown THEN LOOP;
IF vs[n.voice].grace THEN vs[n.voice].graced ← TRUE;
IF n.grace THEN {
vs[n.voice].found ← TRUE;
vs[n.voice].sum ← vs[n.voice].sum+ vs[n.voice].duration-10;
vs[n.voice].duration ← 10;
LOOP};
duration ← Note.Duration[n,m];
IF vs[n.voice].found=FALSE THEN {
vs[n.voice].sum ← vs[n.voice].sum+ vs[n.voice].duration;
vs[n.voice].duration ← duration;
vs[n.voice].found ← TRUE;
IF n.rest AND n.value=whole THEN {
vs[n.voice].sum ← vs[n.voice].sum+ vs[n.voice].duration/2;
vs[n.voice].duration ← vs[n.voice].duration/2}};
IF vs[n.voice].duration>duration THEN vs[n.voice].duration←duration;
ENDLOOP;
--"grace" is set separately to avoid having a second grace note appear to be "graced" by the first.
FOR i IN [0..syncLength) DO
IF (n←s.event[i])=NIL THEN EXIT;
vs[n.voice].grace ← n.grace;
ENDLOOP;
--measures are treated special
IF Measure[s.type] THEN FOR i IN [0..maxVoice] DO
vs[i].found ← TRUE;
vs[i].sum ← vs[i].sum+vs[i].duration;
vs[i].duration←0;
--
vs[i].graced←FALSE;
--
IF vs[n.voice].grace THEN vs[n.voice].graced←TRUE;
--
vs[n.voice].grace←FALSE;
ENDLOOP;
-- determine the max "time" for this sync
max ← 0;
FOR i IN [0..maxVoice] DO
IF NOT vs[i].found AND vs[i].duration#0 THEN offset←10 ELSE offset←0;
max ← MAX[max,vs[i].sum+ offset];
ENDLOOP;
IF max=0 THEN RETURN;
-- align all of the sums
IF ~separate THEN FOR i IN [0..maxVoice] DO
IF vs[i].found THEN vs[i].sum ← max;
ENDLOOP;
END;

--ClearState:PUBLIC PROCEDURE[vs:StatePTR]=INLINE {vs←ALL[[FALSE,FALSE,FALSE,0,0]]};

--*****************************************************************
--realignning voices
--*****************************************************************

Correct:PUBLIC PROCEDURE[time1,time2:Time] =
BEGIN
s,i:CARDINAL;
begin,end:CARDINAL←0;
sumTS,sum:Time←0;
ts:TimeSignature;
vs:State;
FOR s DECREASING IN [0..scoreLength) DO
IF Measure[score[s].type] AND begin=0 THEN {begin←s; LOOP};
IF NOT Measure[score[s].type] THEN LOOP;
end ← begin;
begin ← s;
IF score[begin].time<time1-1 OR score[end].time>time2+1 THEN LOOP;
--we have a candidate measure
SeparateGraceNotes[@begin,@end]; -- may update end
sum ← Sum[begin,end,FALSE,@vs];
ts ← Score.GetTimeSignature[score[end].time];
sumTS←InlineDefs.LongMult[ts.top*256,64/ts.bottom];
IF sum-sumTS< 9 THEN LOOP; -- no complaints
[]←Sum[begin,end,TRUE,@vs];
voicefull ← ALL[FALSE];
FOR i IN [0..maxVoice] DO
IF ABS[vs[i].sum-sumTS]<9 THEN voicefull[i] ← TRUE;
ENDLOOP;
BreakUpSyncs[@begin,@end]; -- may update end
ReconstructVoices[begin,end];
ENDLOOP;
Piece.CleanUpSyncs[score];
END;

SeparateGraceNotes:PROCEDURE[start,stop:POINTER TO CARDINAL] =
BEGIN
n:NotePTR;
sync:SyncPTR;
skip:CARDINAL←0;
grace,normal:BOOLEAN;
FOR i:CARDINAL IN [start↑..scoreLength) DO
IF i=stop↑ THEN EXIT;
IF skip>0 THEN {skip←skip-1; LOOP}; -- skip the syncs that we added
IF score[i].type#notes THEN LOOP;
-- separate grace notes if there are non-grace notes
grace ← normal ← FALSE;
FOR j:CARDINAL IN [0..syncLength) DO
IF (n←score[i].event[j])=NIL THEN EXIT;
IF n.grace THEN grace←TRUE ELSE normal←TRUE;
IF NOT (grace AND normal) THEN LOOP;
sync ← Utility.NewSync[]; --separate
sync.time ← score[i].time;
FOR k:CARDINAL DECREASING IN [0..syncLength) DO
IF (n←score[i].event[k])=NIL THEN LOOP;
IF ~n.grace THEN LOOP;
Sync.AddNote[sync,n];
ENDLOOP;
Piece.AddSync[score,sync];
stop↑ ← stop↑+1;
skip ← skip+1;
EXIT; ENDLOOP;
ENDLOOP;
END;

BreakUpSyncs:PROCEDURE[start,stop:POINTER TO CARDINAL] =
BEGIN
vs:State;
n:NotePTR;
sync:SyncPTR;
skip:CARDINAL←0;
ClearState[@vs];
FOR i:CARDINAL IN (start↑..scoreLength) DO
IF i=stop↑ THEN EXIT;
IF skip>0 THEN {skip←skip-1; LOOP}; -- skip the syncs that we added
IF score[i].type#notes THEN LOOP;
-- separate voices if they have different times
[]←SetState[@vs,score[i],128,TRUE];
FOR v1:CARDINAL IN [0..maxVoice] DO
IF NOT (voicefull[v1] AND vs[v1].found) THEN LOOP;
FOR v2:CARDINAL IN (v1..maxVoice] DO
IF NOT (voicefull[v2] AND vs[v2].found) THEN LOOP;
IF ABS[vs[v1].sum-vs[v2].sum]<9 THEN LOOP; -- same time (close enough)
sync ← Utility.NewSync[];
sync.time ← score[i].time;
FOR j:CARDINAL DECREASING IN [0..syncLength) DO
IF (n←score[i].event[j])=NIL THEN LOOP;
IF n.voice#v1 THEN LOOP;
Sync.AddNote[sync,n];
ENDLOOP;
Piece.AddSync[score,sync];
stop↑ ← stop↑ +1;
skip ← skip+1;
ENDLOOP;
ENDLOOP;
ENDLOOP;
END;

ReconstructVoices:PROCEDURE[start,stop:CARDINAL] =
BEGIN -- merge things that should be together
v1,v2:CARDINAL;
FOR v1 IN [0..maxVoice] DO
IF NOT voicefull[v1] THEN LOOP;
FOR v2 IN (v1..maxVoice] DO
IF voicefull[v2] THEN AdjustVoices[v1,v2,start,stop];
ENDLOOP;
ENDLOOP;
END;

AdjustVoices:PROCEDURE[a,b:CARDINAL,start,stop:CARDINAL] =
BEGIN
--step through the two voices assynchronously.
--adjust them according to their relative order and relative sums.
A,B:State;
n:NotePTR;
time:Time←0;
temp:SyncPTR;
as,bs:CARDINAL←start;
getNextA,getNextB:BOOLEAN;
ClearState[@A];
ClearState[@B];
WHILE as#stop AND bs#stop DO
getNextA ← A[a].sum<=B[b].sum;
getNextB ← B[b].sum<=A[a].sum;
IF getNextA THEN
FOR i:CARDINAL IN (as..stop+1] DO
IF i=stop+1 THEN {Error; EXIT}; -- catches infinite loops
[]←SetState[@A,score[i],128,TRUE];
IF score[i].type#notes AND i#stop THEN LOOP;
IF ~A[a].found THEN LOOP;
as ← i; EXIT;
ENDLOOP;
IF getNextB THEN
FOR i:CARDINAL IN (bs..stop+1] DO
IF i=stop+1 THEN {Error; EXIT}; -- catches infinite loops
[]←SetState[@B,score[i],128,TRUE];
IF score[i].type#notes AND i#stop THEN LOOP;
IF ~B[b].found THEN LOOP;
bs ← i; EXIT;
ENDLOOP;
IF ABS[A[a].sum-B[b].sum]<5 THEN A[a].sum ← B[b].sum ← MAX[A[a].sum,B[b].sum];
--
problem: syncs are out of order
--solution: put the latter one just before the former
IF A[a].sum<B[b].sum AND bs<as THEN
BEGIN
temp ← score[as];
FOR i:CARDINAL DECREASING IN [bs..as) DO score[i+1] ← score[i]; ENDLOOP;
score[bs] ← temp;
as ← bs; bs ← bs+ 1;
END;
IF A[a].sum>B[b].sum AND bs>as THEN
BEGIN
temp ← score[bs];
FOR i:CARDINAL DECREASING IN [as..bs) DO score[i+1] ← score[i]; ENDLOOP;
score[as] ← temp;
bs ← as; as ← as+ 1;
END;
--
problem: notes which should be synced, aren’t.
--solution: put the notes in the earlier sync

IF A[a].sum=B[b].sum AND as#bs THEN IF bs<as
THEN FOR j:CARDINAL DECREASING IN [0..syncLength) DO
IF (n←score[as].event[j])=NIL THEN LOOP;
IF n.voice#a THEN LOOP;
Sync.AddNote[score[bs],n]; -- mustn’t remove sync if it becomes empty!
ENDLOOP
ELSE FOR j:CARDINAL DECREASING IN [0..syncLength) DO
IF (n←score[bs].event[j])=NIL THEN LOOP;
IF n.voice#b THEN LOOP;
Sync.AddNote[score[as],n];
ENDLOOP;
IF A[a].sum=B[b].sum AND bs<as THEN as←bs;
IF A[a].sum=B[b].sum AND as<bs THEN bs←as;
--
problem: notes which shouldn’t be synced, are.
--solution: already taken care of by BreakUpSyncs
ENDLOOP;
END;

voicefull:ARRAY [0..10) OF BOOLEAN;

END.