1 program prepmx;
2 uses control, strings, globals, preamble, lyrics, mtx, analyze,
3   mtxline, status, uptext, notes, files, utility;
4 { fpc mistakenly thinks CONTROL and GLOBALS are not used }
5 
6 {* M-Tx preprocessor to PMX     Dirk Laurie }
7 const version = '0.60';
8       version_date = '<16 March 2005>';
9 
10 {* See file "Corrections" for updates later than those listed below
11 }
12 
13 {* Changes since 0.52b
14    Uses mtx.tex file
15    More than one # or % per word treated correctly in uptext
16    Better way of handling line numbers in lyrics
17 }
18 
19 {* To do next:
20    Take notice of recent improvements in musixtex, pmx and musixlyr
21 {* Current bugs:
22 {* Old bugs:
23    Does not check PMX syntax
24 {* Weaknesses that may later cause bugs:
25    Decisions sometimes made on note, sometimes on scan
26    Can 'here' overrun?
27    Where does one note stop and the next begin?
28    See comment on '!' in uptext
29 }
30       chord_flat = 't';
31       chord_left = 'l';
32       blind = true;
33       visible = false;
34 
35 var
36   last_bar: boolean;
37   multibar: string;
38   repeat_sign: string;
39   bar_of_line, bars_of_rest, rest_spacing: integer;
40 
41 { --------------- Bars and rests --------------- }
42 
43 procedure writeRepeat(var bar: string);
44   var repcode: string;
45   begin  if bar='' then exit;
46     repcode:='';
47     if bar='||' then repcode:='Rd'
48     else if (bar='|]') then repcode:='RD'
49     else if bar='|:' then repcode:='Rl'
50     else if bar=':|:' then repcode:='Rlr'
51     else if bar=':|' then repcode:='Rr'
52     else if last_bar and (bar='|') then repcode:='Rb';
53     if repcode<>'' then putLine(' '+repcode);
54     bar:='';
55   end;
56 
57 procedure supplyRests(voice: voice_index);
58 begin
59   if multi_bar_rest then
60   begin put(multibar,putspace); exit; end;
61   if (bar_of_line=1) and pedanticWarnings then
62   begin  write('Bar ', bar_no, ' Voice ', voice);
63     warning(' Filling missing voice with rests',not print);
64   end;
65   if pickup>0 then
66   put(rests(pickup,meterdenom,visible),nospace);
67   put(pause+' ',putspace);
68 end;
69 
70 procedure countBars(note: string);
71   var k, adjust, sign: integer;
72 begin  predelete(note,2); k:=pos1('+',note); sign:=1;
73   if k=0 then
74   begin k:=pos1('-',note); if k>0 then sign:=-1; end;
75   if k>0 then
76   begin note[k]:='/'; getTwoNums(note,bars_of_rest,adjust); end
77   else begin getNum(note,bars_of_rest); adjust:=0; end;
78   rest_spacing := 14 + adjust*sign;
79 end;
80 
81 (* FIXME  This procedure had a serious bug.  It now has a smaller bug.
82   Basically there should be a test on whether a break between staves
83   is a break between instruments, in which case the procedure is (now)
84   right; or a break between staves of the same instrument, in which
85   case a '|' instead of a '&' should be output (as it was).  If the Space
86   feature is not used, the bug does not show up. *)
87 procedure putMBRest;
88   var i: stave_index;
89 begin
90   put('\\\def\atnextbar{\znotes',nospace);
91   for i:=1 to nstaves do
92   begin put('\mbrest{'+toString(bars_of_rest)+'}{'+toString(rest_spacing)
93      +'}0',nospace);
94     if i<nstaves then put('&',nospace) else putLine('\en}\');
95   end;
96   putline('\\\advance\barno'+toString(bars_of_rest-1)+'\');
97   inc(bar_no,bars_of_rest-1);
98 end;
99 
100 { ---------------------------------------------------------------- }
101 
102 procedure processLine(voice: voice_index; bar_no: integer);
103   var chords, note, cutnote, pretex: string;
104       par_line: paragraph_index;
105       ngrace, nmulti: integer;
106       no_chords, no_uptext: boolean;
107 
108   procedure output(note: string);
109   begin if note<>'' then
110     begin  { pretex is saved up until a spacing note or rest comes }
111       if endsWith(note,'\') and (note[2]<>'\') then
112       begin  curtail(note,'\');  pretex:=pretex+note; note:=''; exit end;
113       if (pretex<>'') and isNoteOrRest(note) then
114       begin  note:=pretex+'\ '+note;  pretex:=''  end;
115       if (outlen>0) and (note[1]<>' ') then note:=' '+note;
116       put(note,nospace);
117     end;
118     if thisNote(voice)=nextvoice then putLine('');
119   end;
120 
121   procedure maybeDotted(var note: string);
122   begin if length(note)<2 then exit;
123     if note[2]='"' then
124     begin note[2]:=note[1]; pretex:=pretex+'\mtxDotted' end
125   end;
126 
127   procedure addChords;
128     var w, nt, labels: string;
129         j, mus_line: integer;
130         chord_line: paragraph_index0;
131         arpeggio, tieme: boolean;
132         lab: char;
133         pitches: int5;
134     procedure outChord;
135       var k: integer;
136     begin if nt='' then exit; tieme:=nt[1]='{'; if tieme then predelete(nt,1);
137       if (nt='') or (nt[1]<'a') or (nt[1]>'g') then
138         error('Must have a note name at the start of "'+nt
139            +'", not "'+nt[1]+'"',print);
140       renewChordPitch(voice,nt);
141       chords := chords+' z'+nt;  nt:='';
142       for k:=1 to length(labels) do if chordPitch(voice)=pitches[k] then
143       begin chords:=chords+' s'+labels[k]+'t'; labels[k]:=' ' end;
144       if tieme then begin chordTie(voice,lab); chords:=chords+' s'+lab+'t' end
145     end;
146   begin  saveStatus(voice);
147     getChordTies(voice, pitches, labels);
148     chords:=''; chord_line:=chordLineNo(voice);
149     if chord_line=0 then no_chords:=true;
150     if no_chords then exit;
151     w := getNextWord(P[chord_line],blank,dummy);
152     if (w=barsym) or (w='') then no_chords:=true;
153     if (w=tilde) or no_chords then exit;
154     mus_line:=line_no; line_no:=orig_line_no[chord_line];
155     arpeggio := w[1]='?'; if arpeggio then
156     begin chords:=' ?'; predelete(w,1); end;
157     j:=1;  nt:='';
158     while j<=length(w) do
159     begin
160       if (w[j]='{') or ((nt<>'{') and (w[j]>='a') and (w[j]<='g')) then outChord
161       else if w[j]=chord_flat then w[j]:='f'
162       else if w[j]=chord_left then w[j]:='e';
163       nt:=nt+w[j]; j:=j+1
164     end;
165     if nt<>'' then outChord;  if arpeggio then chords:=chords+' ?';
166     note:=note+chords;
167     for j:=1 to length(labels) do if labels[j]<>' ' then
168       error('Tie on chord note started but not finished',print);
169     line_no:=mus_line;
170   end;
171 
processOthernull172   function processOther(note: string): string;
173   begin
174     case thisNote(voice) of
175 other: if note[1]=grace_group then
176       begin if length(note)=1 then ngrace:=1
177         else ngrace := pos1(note[2],digits);
178         if ngrace>0 then dec(ngrace);
179       end;
180 { For a zword, take note of pitch but do not change the contents }
181 { Add spurious duration because repich expects duration to be present }
182 zword: begin cutnote:=note; predelete(cutnote,1); insertchar('4',cutnote,2);
183          checkOctave(voice,cutnote); renewPitch(voice,cutnote)
184        end;
185 lyrtag: extractLyrtag(voice,note);
186 rbrac:  endBeam(voice);
187 rparen, rlparen: endSlur(voice,note);
188 lbrac: beginBeam(voice,note);
189 lparen: begin maybeDotted(note); beginSlur(voice,note); end;
190 mword: error3(voice,'Meter change must be the first word of its bar');
191 atword: lyricsAdjust(voice, note);
192     end;
193     processOther:=note;
194   end;
195 
196   procedure lookahead;
197   begin while bind_left[nextNote(voice)] do
198       note:=note+' '+processOther(getMusicWord(voice));
199   end;
200 
201  var l: integer;
202      in_group: boolean;
203 
204   procedure processNote;
205   begin
206     begin if hasVerseNumber(voice) then pretex:=pretex+'\mtxVerse';
207       l := pos1(multi_group,note);
208       if l>0 then scan1(note,l+1,nmulti);
209       activateBeamsAndSlurs(voice);
210       in_group:=false;
211       if ngrace>0 then begin in_group:=true; dec(ngrace) end else
212       if nmulti>0 then begin in_group:=true; dec(nmulti);  end;
213       checkOctave(voice,note);  renewPitch(voice,note);
214       if not in_group then
215       begin resetDuration(voice,durationCode(note)); markDebeamed(voice,note) end;
216       lookahead; getSyllable(voice,pretex);
217       addUptext(voice, no_uptext, pretex);
218       addChords;
219     end;
220   end;
221 
222 begin
223   pretex:='';   no_chords:=false; no_uptext := false;
224   par_line:=musicLineNo(voice);
225   nmulti :=0;  ngrace := 0;  line_no:=orig_line_no[par_line];
226   repeat note:=getMusicWord(voice);  if note='' then exit;
227     if debugMode then writeln(voice,' ',note);
228     case thisNote(voice) of
229   rword: begin  if multi_bar_rest then
230            begin countBars(note); note:=multibar;
231              if uptextOnRests then
232                addUptext(voice, no_uptext, pretex);
233            end
234            else begin
235              l := pos1(multi_group,note);
236              if l>0 then scan1(note,l+1,nmulti);
237              if nmulti>0 then begin in_group:=true; dec(nmulti);  end;
238              if uptextOnRests then
239                addUptext(voice, no_uptext, pretex);
240              if not isPause(note) then resetDuration(voice,durationCode(note));
241            end
242          end;
243   abcdefg: processNote;
244   barword: begin
245       if voice=nvoices then
246       if endOfBar(voice,bar_no) then repeat_sign := note
247         else writeRepeat(note);
248       if note<>barsym then note:='';
249       no_chords:=false;
250     end;
251   FirstOnly: if voice<>nvoices then note:='' else
252       note:=processOther(note);
253       else note:=processOther(note);
254     end;
255     output(note);
256   until endOfBar(voice,bar_no);
257   if not no_chords then skipChordBar(voice);
258 end;
259 
260 { ------------------------------------------------------------------- }
261 
262 procedure getMeterChange(voice: voice_index; var new_meter: string);
263   var pn1, pn2: integer;
264       w, new_command: string;
265 begin  if nextNote(voice)<>mword then exit;
266   w:=getMusicWord(voice);
267   getMeter(w, meternum, meterdenom, pn1, pn2);
268   full_bar := meternum*(64 div meterdenom);
269   new_command := meterWord(meternum, meterdenom, pn1, pn2);
270   if (new_meter<>'') and (new_meter<>new_command)
271     then error3(voice,'The same meter change must appear in all voices');
272   new_meter:=new_command;
273 end;
274 
275 procedure musicParagraph;
276   var j, nvoice: voice_index0;
277       new_meter, lyrassign: string;
278 
279   procedure putPMXlines;
280     var i: paragraph_index;
281   begin  for i:=1 to para_len do
282     if startsWith(P[i],double_comment) then
283     begin  predelete(P[i],2);  putLine(P[i]);  P[i]:='%';  end;
284   end;
285 
286   procedure processOneBar;
287     var m, cm: paragraph_index0;
288         n1, n2: integer;
289         voice, cvoice: voice_index;
290         ignore_voice, wrote_repeat, alone: boolean;
291   begin
292     if bar_of_line>1 then putLine(comment+'Bar '+toString(bar_no));
293     last_bar := (bar_of_line=nbars) and final_paragraph;
294     if last_bar and (repeat_sign='|') then repeat_sign:='';
295     writeRepeat(repeat_sign);  new_meter := '';
296     for voice:=nvoices downto 1 do
297     if musicLineNo(voice)>0 then
298     begin gotoBar(voice,bar_of_line); getMeterChange(voice,new_meter);
299     end;
300     if last_bar and (new_meter='') and (nleft>pickup) and (meternum>0) then
301       new_meter := meterChange(nleft,64,true);
302     if new_meter<>'' then putLine(new_meter);
303     wrote_repeat := false;  if multi_bar_rest then
304     begin n1:=64*meternum; n2:=meterdenom; cancel(n1,n2,1);
305        multibar:=rests(n1,n2,blind);
306     end;
307     for voice:=nvoices downto 1 do
308     begin  ignore_voice:=not selected[voice];  cvoice:=companion(voice);
309       m:=musicLineNo(voice); cm:=musicLineNo(cvoice);
310       alone:=(voice=cvoice) or ((m>0) and (cm=0))
311         or ((m=0) and (cm=0) and (voice<cvoice)) or not selected[cvoice];
312       if selected[voice] then
313       begin
314         if m>0 then processLine(voice,bar_of_line)
315           else if alone then supplyRests(voice) else ignore_voice:=true;
316         if last_bar and (repeat_sign<>'') and not wrote_repeat then
317         begin writeRepeat(repeat_sign); wrote_repeat := true; end;
318         if not ignore_voice then
319         if alone or (voicePos(voice)=1) then putLine(' /')
320           else putLine(' //');
321       end;
322     end;
323     if multi_bar_rest then putMBRest;
324     inc(bar_no); pickup:=0; putLine('');
325   end;
326 
327   procedure putMeter(new_meter_word: string);
328   begin if new_meter_word<>old_meter_word then putLine(new_meter_word);
329     old_meter_word := new_meter_word;
330   end;
331 
332 begin
333   paragraphSetup(nvoice);
334   if nvoice=0 then begin  nonMusic; exit end
335   else if nvoice>nvoices then
336   begin  if nvoice=0 then
337     error('No voices! Did you remember to to supply a Style?',not print);
338     error('Paragraph has '+toString(nvoice)+
339     ' voices but Style allows only '+toString(nvoices),not print); exit;
340   end;
341   if first_paragraph then includeStartString;
342   if pmx_preamble_done and (not final_paragraph or (nvoice>0)) then
343     putLine(comment +' Paragraph ' + toString(paragraph_no) +
344    ' line ' + toString(orig_line_no[1]) + ' bar ' + toString(bar_no));
345   testParagraph; rememberDurations;
346   if beVerbose then describeParagraph;
347   { ---- Knowing the score, we can start setting music ---------------- }
348   if not pmx_preamble_done then
349   begin  doPMXpreamble;
350      put( comment + ' Paragraph ' + toString(paragraph_no) + ' line ' +
351        toString(orig_line_no[1]) + ' bar ',putspace);
352      if pickup>0 then putLine('0') else putLine('1');
353   end;
354   putPMXlines;
355   if must_restyle then restyle;
356   if some_vocal and ((nvoice>0) or not final_paragraph) then
357   for j:=1 to ninstr do
358   begin  assignLyrics(j,lyrassign);
359     if lyrassign<>'' then putLine('\\'+lyrassign+'\');
360   end;
361   if must_respace then respace;
362   if (meternum=0) then putMeter(meterChange(beatsPerLine,meterdenom,true));
363   if nleft > 0 then inc(nbars);
364   if (nbars=0) and multi_bar_rest then nbars:=1;
365   for bar_of_line:=1 to nbars do
366     processOneBar;
367   restoreDurations;
368 end;
369 
370 { ----------------------------------------------------------------------- }
371 
372 var no_commands_yet: boolean;
373 
374 procedure doMusic;
375 begin  first_paragraph:=true;  pmx_preamble_done:=false;  bar_no:=1;
376   repeat_sign:='';  must_respace:=false; must_restyle:=false;
377   repeat final_paragraph := endOfInfile;
378     if (para_len>0) and not ignore_input and thisCase then
379     begin  if no_commands_yet then
380       begin interpretCommands;  printFeatures(false);
381         one_beat := 64 div meterdenom;  full_bar := meternum*one_beat;
382         if nvoices>standardPMXvoices then warning('You have '
383            +toString(nvoices)+' voices; standard PMX can only handle '
384            +toString(standardPMXvoices),not print);
385         initMTX; initUptext; initStatus; initLyrics;
386         no_commands_yet:=false
387       end;
388       if startsWithBracedWord(P[1]) then lyricsParagraph else
389       begin musicParagraph;  first_paragraph:=false;
390         writeRepeat(repeat_sign);
391       end
392     end;
393     readParagraph(P,orig_line_no,para_len);
394   until para_len=0;
395 end;
396 
397 var control_paragraph, no_report_errors: boolean;
398 
isControlParagraphnull399 function isControlParagraph (var P: paragraph; para_len: paragraph_index)
400                 : boolean;
401   var commands, labels, voices, guff, i: paragraph_index0;
402       w: string;
403 begin isControlParagraph:=true; commands:=0; labels:=0; voices:=0; guff:=0;
404   for i:=1 to para_len do
405     if not (startsWith(P[i],'%')) then
406     begin w:=nextWord(P[i],' ',':');
407       if not endsWith(w,':') then inc(guff)
408       else if (length(w)<3) or (findVoice(w)>0) then inc(voices)
409       else if isCommand(w) then inc(commands)
410       else inc(labels)
411     end;
412   if (voices+guff>commands) then isControlParagraph:=false
413 end;
414 
415 procedure topOfPMXfile;
416 begin
417   putLine('---');
418   putLine('\def\mtxversion{'+version+'}');
419   putline('\def\mtxdate{'+version_date+'}');
420   putline('\input mtx');
421 end;
422 
423 begin   { ---- Main program ------------------------ }
424   this_version := version;  this_version_date := version_date;
425   writeln ('==> This is M-Tx ' + version + ' (Music from TeXt) ' +
426     version_date );
427   mtxLevel(version);  OpenFiles;  no_commands_yet:=true;  preambleDefaults;
428   no_report_errors:=false;
429   topOfPMXfile;
430   repeat readParagraph(P,orig_line_no,para_len);
431     control_paragraph:=isControlParagraph(P,para_len);
432     if control_paragraph then
433     begin  augmentPreamble(no_report_errors);
434       no_report_errors:=true;
435       if para_len=0 then error('No music paragraphs!',not print)
436     end
437   until not control_paragraph;
438   doPreamble; doMusic;
439   if not pmx_preamble_done then error('No music paragraphs!',not print);
440   putline('% Coded by M-Tx');
441   CloseFiles;
442   writeln ('PrePMX done.  Now run PMX.');  halt(0);
443 end.
444 
445