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