-- Author: John Maxwell
-- Last Edited by: Maxwell, November 22, 1983 8:37 am

DIRECTORY
	Event USING [AddNote, Sync], 
	Inline USING [LongMult], 
	MusicDefs, 
	Note USING [Duration], 
	Piece USING [AddEvent, CleanUpEvents, Overflow], 
	Score USING [GetTimeSignature], 
	Selection USING [Enumerate], 
	Sheet USING [HiLite], 
	Voice USING [ClearState, max, State, StatePTR];

VoiceImpl: PROGRAM 
  IMPORTS Inline, MusicDefs, Note, Piece, Score, Selection, Sheet, Event, Voice 
  EXPORTS Voice = 
BEGIN
OPEN MusicDefs, Voice;

Error: SIGNAL = CODE;

-- voice: PUBLIC BOOLEAN ← FALSE;
-- selectedVoice: PUBLIC CARDINAL ← 0;
-- maxVoice: PUBLIC CARDINAL ← 0;
	
Set: PUBLIC PROCEDURE[score: ScorePTR, voice: CARDINAL] = 
BEGIN
SetVoice: PROC[score: ScorePTR, n: NotePTR] = {n.voice ← voice};
Selection.Enumerate[SetVoice];
score.maxVoice ← MAX[voice, score.maxVoice];
END;

Check: PUBLIC PROCEDURE[score: ScorePTR] = 
BEGIN
vs: State;
sum, sumTS: Time ← 0;
begin, end: CARDINAL ← 0;
FOR i: CARDINAL IN [0..score.length) DO
	IF score[i].type = timeSignature THEN {
		timeSig: LONG POINTER TO EventRec.timeSignature ← LOOPHOLE[score[i]];
		sumTS ← Inline.LongMult[timeSig.ts.top*256, 64/timeSig.ts.bottom]};
	IF ~Measure[score[i]] THEN LOOP;
	begin ← end; end ← i; 
	IF begin = end THEN LOOP;
	sum ← Sum[score, begin, end, score.sheet.voice # noVoice, @vs];
	IF sum = 0 THEN LOOP;
	IF ABS[sum-sumTS] > 8 THEN 
		Sheet.HiLite[score.sheet, lightgrey, score[begin].time, score[end].time];
	ENDLOOP;
END;


lightgrey: CARDINAL = 004040B;    

Sum: PROCEDURE[score: ScorePTR, 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 # sync THEN LOOP; 
	[] ← SetState[ss, score[i], 128, separate];
	ENDLOOP;
[] ← SetState[ss, score[end], 128, separate];
FOR v: CARDINAL IN [0..Voice.max) DO
	IF separate AND v # score.sheet.voice THEN LOOP; 
	sum ← MAX[sum, ss[v].sum+ss[v].duration]; 
	ENDLOOP;
END;

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

SetState: PUBLIC PROCEDURE[vs: StatePTR, event: EventPTR, 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;
sync: SyncPTR ← NIL;
IF event.type = sync THEN sync ← Event.Sync[event];
FOR i: CARDINAL IN [0..10) DO vs[i].found ← vs[i].graced ← FALSE; ENDLOOP;
IF sync # NIL THEN FOR i: CARDINAL IN [0..sync.length) DO
	IF (n ← sync.note[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.
IF sync # NIL THEN FOR i: CARDINAL IN [0..Voice.max) DO
	IF (n ← sync.note[i]) = NIL THEN EXIT;
	vs[n.voice].grace ← n.grace;
	ENDLOOP;
-- measures are treated special
IF Measure[event] THEN FOR i: CARDINAL IN [0..Voice.max) 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: CARDINAL IN [0..Voice.max) 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: CARDINAL IN [0..Voice.max) 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[score: ScorePTR, time1, time2: Time] = 
BEGIN ENABLE Piece.Overflow => IF old = score THEN score ← new;
begin, end: CARDINAL ← 0;
sumTS, sum: Time ← 0;
ts: TimeSignature;
vs: State;
FOR i: CARDINAL DECREASING IN [0..score.length) DO
	IF Measure[score[i]] AND begin = 0 THEN {begin ← i; LOOP};
	IF NOT Measure[score[i]] THEN LOOP;
	end ← begin;
	begin ← i;
	IF score[begin].time < time1-1 OR score[end].time > time2+1 THEN LOOP;
	-- we have a candidate measure
	SeparateGraceNotes[score, @begin, @end]; -- may update end
	sum ← Sum[score, begin, end, FALSE, @vs];
	ts ← Score.GetTimeSignature[score, score[end].time];
	sumTS ← Inline.LongMult[ts.top*256, 64/ts.bottom];
	IF sum-sumTS < 9 THEN LOOP; -- no complaints 	   
	[] ← Sum[score, begin, end, TRUE, @vs];
	FOR v: CARDINAL IN [0..Voice.max) DO
		vs[v].full ← ABS[vs[i].sum-sumTS] < 9; 
		ENDLOOP;
	BreakUpEvents[score, @begin, @end]; -- may update end
	ReconstructVoices[score, @vs, begin, end];
	ENDLOOP;
Piece.CleanUpEvents[score];
END;

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

BreakUpEvents: PROCEDURE[score: ScorePTR, start, stop: POINTER TO CARDINAL] = 
BEGIN 
vs: State;
n: NotePTR;
sync, new: SyncPTR;
skip: CARDINAL ← 0;
ClearState[@vs];
FOR i: CARDINAL IN (start↑..score.length) DO
	IF i = stop↑ THEN EXIT;
	IF skip > 0 THEN {skip ← skip-1; LOOP}; -- skip the syncs that we added
	IF score[i].type # sync THEN LOOP;
	sync ← Event.Sync[score[i]];
	-- separate voices if they have different times
	[] ← SetState[@vs, score[i], 128, TRUE];
	FOR v1: CARDINAL IN [0..Voice.max) DO
		IF NOT (vs[v1].full AND vs[v1].found) THEN LOOP;
		FOR v2: CARDINAL IN (v1..Voice.max) DO
			IF NOT (vs[v2].full AND vs[v2].found) THEN LOOP;
			IF ABS[vs[v1].sum-vs[v2].sum] < 9 THEN LOOP; -- same time (close enough) 
			new ← zone.NEW[EventRec.sync];
			new.time ← score[i].time;
			FOR j: CARDINAL DECREASING IN [0..sync.length) DO
				IF (n ← sync.note[j]) = NIL THEN LOOP;
				IF n.voice # v1 THEN LOOP;
				new ← Event.AddNote[score, new, n];
				ENDLOOP;
			score ← Piece.AddEvent[score, new];
			stop↑ ← stop↑ +1;
			skip ← skip+1;
			ENDLOOP;
		ENDLOOP;
	ENDLOOP;
END;

ReconstructVoices: PROCEDURE[score: ScorePTR, vs: StatePTR, start, stop: CARDINAL] = 
BEGIN -- merge things that should be together
FOR v1: CARDINAL IN [0..Voice.max) DO
	IF NOT vs[v1].full THEN LOOP;
	FOR v2: CARDINAL IN (v1..Voice.max) DO
		IF vs[v2].full THEN AdjustVoices[score, v1, v2, start, stop];
		ENDLOOP;
	ENDLOOP;
END;

AdjustVoices: PROCEDURE[score: ScorePTR, 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: EventPTR;
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 # sync 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 # sync 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..Event.Sync[score[as]].length) DO
			IF (n ← Event.Sync[score[as]].note[j]) = NIL THEN LOOP;
			IF n.voice # a THEN LOOP;
			score[bs] ← Event.AddNote[score, Event.Sync[score[bs]], n]; 
			-- mustn't remove score[as] if it becomes empty!
			ENDLOOP
		ELSE FOR j: CARDINAL DECREASING IN [0..Event.Sync[score[bs]].length) DO
			IF (n ← Event.Sync[score[bs]].note[j]) = NIL THEN LOOP;
			IF n.voice # b THEN LOOP;
			score[as] ← Event.AddNote[score, Event.Sync[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 BreakUpEvents
ENDLOOP;
END;

END.