An automatic guitar tablature generator, part 2
Last month, I presented the code for a blank guitar tablature page generator. This becomes much more interesting if it can also render tablature based on user input. As I mentioned last month, there are a lot of "ASCII-art" tablature transcriptions available for download on the internet — it would be cool to automatically parse those and turn those into print-ready tablature, along with the musical notation above it as in professional tablature.
I won't present a full ASCII-art tablature parser here, but sort of a middle-of-the-road utility that takes as input triplets indicating the guitar string (E,A,D,G,B or e), the fret that that string should be played on and the duration of the note. So, for instance, the opening riff to Guns and Roses "Sweet Child O' Mine" would be input as:
- 8,D,12
- 8,B,14
- 8,B,12
- 8,D,12
- 8,e,15
- 8,G,13
- 8,e,14
- 8,G,13
These I'll encapsulate in a new class named Note
along with a couple of
Enum
s that represent guitar strings and note durations as shown in listing 1:
enum GuitarString {
E(0,"E"),A(1,"A"),D(2,"D"),G(3,"G"),B(4,"B"),e(5,"e");
public int offset;
public String name;
GuitarString(int offset, String name) {
this.offset = offset;
this.name = name;
}
public String toString() {
return name;
}
};
enum Duration {
whole(1), half(2), quarter(4), eighth(8), sixteenth(16);
public int frequency;
Duration(int frequency) {
this.frequency = frequency;
}
};
class Note {
GuitarString str;
int fret;
Duration dur;
public Note(GuitarString str, int fret, Duration dur) {
this.str = str;
this.fret = fret;
this.dur = dur;
}
...
Given the string and the fret number, it's an easy matter to output the tablature. Recall
from last month that the tablature section is six lines (one for each guitar string) of
height tabGap
— since a guitar string can report its "offset" relative to
the low E-string, rendering the tablature of a note is a simple multiplication as shown in
listing 2.
public void renderTab(PdfContentByte canvas, float x, float y) {
float offset = str.offset * TabWriter.tabGap;
canvas.showTextAligned(PdfContentByte.ALIGN_LEFT, Integer.toString(fret),
x, y - 2 + offset, 0);
}
renderTab
accepts as input a PdfContentByte
instance from the
iText
library as I discussed last month. This is the same object that is
responsible for implementing the line-drawing primitives that output the tablature lines
themselves. showTextAligned
is a new method, though. Although its behavior
is pretty self-explanatory, it will maddeningly output nothing unless boxed in between a pair
of beginText
/endText
invocations. So, given an array of
Note
objects, assuming that the tablature lines have already been output as discussed
in part 1, the tab can be inserted into the right place as shown in listing 3:
canvas.beginText();
BaseFont bf = BaseFont.createFont(BaseFont.COURIER, BaseFont.CP1252, BaseFont.NOT_EMBEDDED);
canvas.setFontAndSize(bf, 8.0F);
y = pageSize.getHeight() - marginTop - staffHeight;
x = marginLeft + clefWidth;
int measureCount = 0;
for (Note note : measure) {
note.renderTab(canvas, x, y);
x += noteWidth;
}
canvas.fillStroke();
canvas.endText();
This should be inserted right between the canvas.stroke()
call and the
document.close()
invocations from example 4 of last month's post. Of course,
all by itself, this isn't that interesting — hardly better than the ASCII-art
representations you can find all over the internet. To be complete, I should compute the
correct positioning and rendering of the musical note that corresponds to this tablature note.
This turns out to be computationally quite a bit more complex.
First, to make the math easier, I "normalize" everything onto a single, very long, theoretical low-E string and then compute the position of the musical note from there. This is possible because each string on a guitar is strung (in standard tuning) two-and-a-half steps below the one above it, so the pitches repeat if you move up and to the right, as shown in figure 1.
This would then translate to a simple multiplication: the open E-string, fret 0, is an E; the next two are F and F#, the next two after that are G and G#, followed by A and A#, etc. So I could almost multiply the fret number by the height of a music note, then divide by two (to account for the fact that each note on the musical staff appears at the same height as its "sharped" counterpart), and render the note there. This would work, if not for the fact that E and B don't have sharped notes. Thus, to find the compute the correct place to render the music note, I first adjust for the "missing" sharped notes and then multiply. I do this in three separate places, for three separate reasons: first, to determine where to place the note itself. Second, to determine whether or not to draw connecting bars up into the standard staff, and third, to determine if the note should be rendered with a sharp (#) marker.
On a 24-fret guitar (the "longest" guitar I've ever seen), this works out to 48 "virtual" frets, whose notes are named as illustrated in the table below.
E | A | D | G | B | e | Note |
---|---|---|---|---|---|---|
0 | - | - | - | - | - | E |
1 | - | - | - | - | - | F |
2 | - | - | - | - | - | F# |
3 | - | - | - | - | - | G |
4 | - | - | - | - | - | G# |
5 | 0 | - | - | - | - | A |
6 | 1 | - | - | - | - | A# |
7 | 2 | - | - | - | - | B |
8 | 3 | - | - | - | - | C |
9 | 4 | - | - | - | - | C# |
10 | 5 | 0 | - | - | - | D |
11 | 6 | 1 | - | - | - | D# |
12 | 7 | 2 | - | - | - | E |
13 | 8 | 3 | - | - | - | F |
14 | 9 | 4 | - | - | - | F# |
15 | 10 | 5 | 0 | - | - | G |
16 | 11 | 6 | 1 | - | - | G# |
17 | 12 | 7 | 2 | - | - | A |
18 | 13 | 8 | 3 | - | - | A# |
19 | 14 | 9 | 4 | 0 | - | B |
20 | 15 | 10 | 5 | 1 | - | C |
21 | 16 | 11 | 6 | 2 | - | C# |
22 | 17 | 12 | 7 | 3 | - | D |
23 | 18 | 13 | 8 | 4 | - | D# |
24 | 19 | 14 | 9 | 5 | 0 | E |
25 | 20 | 15 | 10 | 6 | 1 | F |
26 | 21 | 16 | 11 | 7 | 2 | F# |
27 | 22 | 17 | 12 | 8 | 3 | G |
28 | 23 | 18 | 13 | 9 | 4 | G# |
29 | 24 | 19 | 14 | 10 | 5 | A |
30 | 25 | 20 | 15 | 11 | 6 | A# |
31 | 26 | 21 | 16 | 12 | 7 | B |
32 | 27 | 22 | 17 | 13 | 8 | C |
33 | 28 | 23 | 18 | 14 | 9 | C# |
34 | 29 | 24 | 19 | 15 | 10 | D |
35 | 30 | 25 | 20 | 16 | 11 | D# |
36 | 31 | 26 | 21 | 17 | 12 | E |
37 | 32 | 27 | 22 | 18 | 13 | F |
38 | 33 | 28 | 23 | 19 | 14 | F# |
39 | 34 | 29 | 24 | 20 | 15 | G |
40 | 35 | 30 | 25 | 21 | 16 | G# |
41 | 36 | 31 | 26 | 22 | 17 | A |
42 | 37 | 32 | 27 | 23 | 18 | A# |
43 | 38 | 33 | 28 | 24 | 19 | B |
44 | 39 | 34 | 29 | 25 | 20 | C |
45 | 40 | 35 | 30 | 26 | 21 | C# |
46 | 41 | 36 | 31 | 27 | 22 | D |
47 | 42 | 37 | 32 | 28 | 23 | D# |
48 | 43 | 38 | 33 | 29 | 24 | E |
The lowest (pitch) note that you can play on a guitar string is a full octave below the low E that appears at the bottom of the standard treble staff. So, given the fret of my now-normalized string, I want to multiply that by half-a-staff height (remember that musical notes appear both on and between the lines of the staff — that's why I was careful to define the staff height as an even number!) for every other fret; G and G# both appear in the same place, but G# has a sharp-marker on it. But each time I encounter an E or a B, I do want to jump ahead right afterwards. This is easier to visualize if you look at the table below:
fret | offset 1 | offset 2 | final offset | note |
---|---|---|---|---|
0 | 0 | 0 | 0 | E |
1 | 0 | 0 | 1 | F |
2 | 0 | 0 | 2 | F# |
3 | 0 | 0 | 3 | G |
4 | 0 | 0 | 4 | G# |
5 | 0 | 0 | 5 | A |
6 | 0 | 0 | 6 | A# |
7 | 0 | 0 | 7 | B |
8 | 1 | 0 | 9 | C |
9 | 1 | 0 | 10 | C# |
10 | 1 | 0 | 11 | D |
11 | 1 | 0 | 12 | D# |
12 | 1 | 0 | 13 | E |
13 | 1 | 1 | 15 | F |
14 | 1 | 1 | 16 | F# |
15 | 1 | 1 | 17 | G |
16 | 1 | 1 | 18 | G# |
17 | 1 | 1 | 19 | A |
18 | 1 | 1 | 20 | A# |
19 | 1 | 1 | 21 | B |
20 | 2 | 1 | 23 | C |
21 | 2 | 1 | 24 | C# |
... |
int adjustedFret = normalizedFret + ((normalizedFret + 5) / 12) + ((normalizedFret + 12) / 12);
In other words, add one step every 12 notes starting with the 5th, and add another every 12
notes starting with the 12th.
This, when divided by two and then multiplied by one-half of the height of a staff marker,
tells me how many pixels to move up past the lowest renderable note in order to render the
current note.
This looks like it could be simplified with a little bit of algebra, but it actually relies on the specific rounding behavior of the two division operations, so won't work unless I leave it as is.
NOW, given the position of the 0 fret (the low E one octave below the lowest on the treble clef),
finding the correct spot to draw the musical note is just a simple multiplication:
Where
float note_y = y + ((adjustedFret / 2) * (TabWriter.staffGap /2));
y
is the vertical coordinate of the lowest note that can be output (again,
the open low E-string).
Modern music notation indicates the duration of each note pictorially; whole and half notes are written out as empty circles, quarter, eighth and sixteenth (and beyond) as filled circles. Further, every note except a whole note has a "stem" and every subdivision below an quarter note adds an extra "flag" as show in listing 4.
canvas.ellipse(x, note_y, x + 7.0f, note_y + 5.0f);
if (dur.frequency > 1) {
if (normalizedFret < 19) {
canvas.moveTo(x + 7.0f, note_y + 3);
canvas.lineTo(x + 7.0f, note_y + 3 + (TabWriter.staffGap * 2.5f));
} else {
canvas.moveTo(x, note_y + 3);
canvas.lineTo(x, note_y + 3 - (TabWriter.staffGap * 2.5f));
}
if (dur.frequency > 4) {
// one flag
if (normalizedFret < 19) {
canvas.rectangle(x + 7.0f, note_y + 3 + (TabWriter.staffGap * 2.0f), 8, 4);
} else {
canvas.rectangle(x - 8.0f, note_y - (TabWriter.staffGap * 2.0f), 8, 4);
}
if (dur.frequency > 8) {
// two flags
if (normalizedFret < 19) {
canvas.rectangle(x + 7.0f, note_y + 3 + (TabWriter.staffGap * 1.0f), 8, 4);
} else {
canvas.rectangle(x - 8.0f, note_y - (TabWriter.staffGap * 1.0f), 8, 4);
}
}
}
}
However, this doesn't quite render correctly, at least for the notes that appear above and below the treble clef (which, on a guitar, are more than half of them). Those notes should be connected to the staff with leading lines as illustrated in figure 2.
Since I computed the y
position of the note in listing 3, it's easy enough
to count down or up from the top or the bottom of the staff to it, adding lines as I go:
int staffBottom = (int) (y + (TabWriter.staffGap * 3));
int staffTop = (int) (staffBottom + (TabWriter.staffGap * 5));
int additional_lines_y = staffBottom; // - (int) TabWriter.staffGap;
while ((additional_lines_y - TabWriter.staffGap) >= note_y) {
canvas.moveTo(x, additional_lines_y);
canvas.lineTo(x + 7.0f, additional_lines_y);
additional_lines_y -= TabWriter.staffGap;
}
additional_lines_y = staffTop + (int) TabWriter.staffGap;
while ((additional_lines_y + TabWriter.staffGap) <= note_y) {
canvas.moveTo(x, additional_lines_y);
canvas.lineTo(x + 7.0f, additional_lines_y);
additional_lines_y += TabWriter.staffGap;
}
I'm still not quite done here, though — the sharped notes don't have their
sharp markers!
Now, in the case of sharped notes, the note is sharped if the fretted position is even when
between 1 and 7, odd when between 8 and 12, even when between 13 and 19, odd when between 20 and
24, and so on. I can determine whether a note should be sharped using:
Which, in this case, can be simplified algebraically to:
((((normalizedFret % 12) + (((normalizedFret % 12) + 6) / 7)) % 2) == 1)
if ((((8 * (normalizedFret % 12) + 6) / 7) % 2) == 1) {
canvas.showTextAligned(PdfContentByte.ALIGN_LEFT, "#",
x + 7.0f, note_y, 0);
}
Here, I just output the octothorpe (hexadecimal 23) rather than the official Unicode sharp marker U+266E.
So here I now have enough to put together a somewhat respectable-looking tablature sheet. I've talked about how to represent and render a single note, but all music (all interesting music anyway) consists of at least a few notes grouped into measures. In addition to this, a guitar allows for as many as six distinct notes to be played simultaneously - that is, it allows for chords to be played. Hence, my internal representation for a piece of guitar music is a triply-nested array of array of arrays of notes: the inner array (which may consist of just a single note) is a chord, the next-level array is a measure — a collection of chords/notes — and finally the outer array is a collection of measures. I suppose I could be more object-orientedly correct here and declare actual types for these concepts, but for now a triply-nested array seems to do fine.
All that's left is to iterate over this triply-nested array structure, invoking
renderTab
and renderNote
once for each note, adjusting the x
and y
values along the way. I go through and output the tablature first, and then
start back at the top and render the notes. This is because of an oddity in the iText interface
(and the underlying PDF format) — I can output all of the textual tablature at one time
to speed things up, but I have to output each note individually; that's the only way to cause
some to appear filled in and some to appear hollow.
Listing 6 puts the whole thing together; the notes themselves are embedded in the code listing here, but it would be a simple matter to read in a textual input file and dynamically create the input array from it.
import java.io.IOException;
import java.io.FileOutputStream;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfWriter;
import com.itextpdf.text.pdf.CMYKColor;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfContentByte;
enum GuitarString {
E(0,"E"),A(1,"A"),D(2,"D"),G(3,"G"),B(4,"B"),e(5,"e");
public int offset;
public String name;
GuitarString(int offset, String name) {
this.offset = offset;
this.name = name;
}
public String toString() {
return name;
}
};
enum Duration {
whole(1), half(2), quarter(4), eighth(8), sixteenth(16);
public int frequency;
Duration(int frequency) {
this.frequency = frequency;
}
};
class Note {
GuitarString str;
int fret;
Duration dur;
public Note(GuitarString str, int fret, Duration dur) {
this.str = str;
this.fret = fret;
this.dur = dur;
}
/**
* Show this note on an already-rendered staff. Draw the fret and string positions
* on the tablature part.
* @param canvas an open "pdf" canvas
* @param x the x coordinate, relative to the page, of the bottom part of the
* tablature marker. The note should be rendered immediately above this X coordinate
* (and it's the responsibility of the caller to ensure that this values advances
* correctly).
* @param y the y coordinate of the bottom-left part of the tablature marker.
*/
public void renderTab(PdfContentByte canvas, float x, float y) {
float offset = str.offset * TabWriter.tabGap;
canvas.showTextAligned(PdfContentByte.ALIGN_LEFT, Integer.toString(fret),
x, y - 2 + offset, 0);
}
/**
* Compute the location of the corresponding musical note and draw it above the
* tablature.
* 1. Normalize the location of the note as though it were on a very long low-E
* string.
* 2. The lowest note that a guitar can play is the low E which is three staff heights
* below the standard one.
* If the computed location is above or below the standard treble clef, have to write
* out extra lines to match down to it.
* @param x the x coordinate at which the note should be output; the caller is responsible
* for advancing this once for each note.
* @param y the y coordinate of the lowest note that can be output; the E one octave
* below the first one on the treble clef.
*/
public void renderNote(PdfContentByte canvas, float x, float y)
throws DocumentException, IOException {
// "normalized fret" is where you'd play this note on a theoretically very long
// low E string.
int normalizedFret = fret + (str.offset * 5) - ((str.offset > 3) ? 1 : 0);
// staffBottom points to the lowest note that's still on the staff (for computing
// whether or not to add lines below the staff)
int staffBottom = (int) (y + (TabWriter.staffGap * 4));
int staffTop = (int) (staffBottom + (TabWriter.staffGap * 4));
// Adjust for the places where I jump 'too far' due to the 'missing' sharps.
int adjustedFret = normalizedFret +
((normalizedFret + 5) / 12) +
((normalizedFret + 12) / 12);
// Adjust the y offset, always up
// divide by 2.0 to make ceil work correctly
int halfGap = (int) (TabWriter.staffGap / 2);
float note_y = y + ((adjustedFret / 2) * halfGap);
// note_y points to the _bottom_ of the music note to be drawn
// A music note is just a little wider than it is tall
canvas.ellipse(x, note_y, x + 7.0f, note_y + 5.0f);
if (dur.frequency > 1) {
if (normalizedFret < 19) {
canvas.moveTo(x + 7.0f, note_y + 3);
canvas.lineTo(x + 7.0f, note_y + 3 + (TabWriter.staffGap * 2.5f));
} else {
canvas.moveTo(x, note_y + 3);
canvas.lineTo(x, note_y + 3 - (TabWriter.staffGap * 2.5f));
}
if (dur.frequency > 4) {
// one flag
if (normalizedFret < 19) {
canvas.rectangle(x + 7.0f, note_y + 3 + (TabWriter.staffGap * 2.0f), 8, 4);
} else {
canvas.rectangle(x - 8.0f, note_y - (TabWriter.staffGap * 2.0f), 8, 4);
}
if (dur.frequency > 8) {
// two flags
if (normalizedFret < 19) {
canvas.rectangle(x + 7.0f, note_y + 3 + (TabWriter.staffGap * 1.0f), 8, 4);
} else {
canvas.rectangle(x - 8.0f, note_y - (TabWriter.staffGap * 1.0f), 8, 4);
}
}
}
}
// Is it a sharp? If so, add a sharp sign
if ((((8 * (normalizedFret % 12) + 6) / 7) % 2) == 1) {
canvas.showTextAligned(PdfContentByte.ALIGN_LEFT, "#",
x + 7.0f, note_y, 0);
}
// If there were additional staff lines on the top or bottom, add them here
int additional_lines_y = staffBottom;
int targetNote = (int) note_y; // convert to int for comparison below
while (additional_lines_y > targetNote) {
canvas.moveTo(x - 2.0f, additional_lines_y);
canvas.lineTo(x + 9.0f, additional_lines_y);
additional_lines_y -= TabWriter.staffGap;
}
additional_lines_y = staffTop + (int) TabWriter.staffGap;
while (additional_lines_y <= targetNote) {
canvas.moveTo(x - 2.0f, additional_lines_y);
canvas.lineTo(x + 9.0f, additional_lines_y);
additional_lines_y += TabWriter.staffGap;
}
if (dur.frequency > 2) {
canvas.fillStroke();
} else {
canvas.stroke(); // hollow circles for whole and half notes
}
}
}
/**
* Generate a PDF document with a few rows of blank guitar tablature and fill
* it in with tablature and musical notes.
*/
public class TabWriter {
// Remember that Y starts at the bottom and goes up
public static float marginTop = 35.0f;
public static float marginLeft = 15.0f;
public static float staffGap = 6.0f; // gap between the lines in the music staff
public static float separatorGap = 25.0f; // gap between the music staff and the tab staff
public static float tabGap = 5.0f; // gap between the lines in the tab staff
public static float barGap = 35.0f; // gap between each complete combined staff
// barGap has to leave at least 9 * staffGap/2 pixels available, because the high-E
// string on a 24-fret guitar will appear that high above the staff itself.
public static float clefWidth = 20.0F;
public static void main(String[] args) throws IOException, DocumentException {
if (args.length < 1) {
System.err.println("Usage: TabWriter [output filename]");
System.exit(0);
}
Document document = new Document();
PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(args[0]));
document.open();
PdfContentByte canvas = writer.getDirectContent();
Rectangle pageSize = document.getPageSize();
canvas.setColorStroke(BaseColor.BLACK);
canvas.setLineWidth(1.0F);
float staffWidth = pageSize.getWidth() - (2 * marginLeft);
float staffHeight = (staffGap * 5) + separatorGap + (tabGap * 5);
// Assume four measures per tab line, apportion them out evenly
float measureWidth = (staffWidth - clefWidth) / 4.0F;
float noteWidth = measureWidth / 8.0F; // leave room for eight eighth-notes per measure
// Figure out how many bars will fit comfortably on this page
int barCount = (int) ((pageSize.getHeight() - (marginTop * 2)) /
((staffGap * 5) + separatorGap + (tabGap * 5) + barGap));
// Figure out how much "extra space" is at the bottom and distribute it evenly between
// the staves for aesthetic purposes.
float remainingSpace = (pageSize.getHeight() - (marginTop * 2)) -
(barCount * ((staffGap * 5) + separatorGap + (tabGap * 5) + barGap));
int additionalBarGap = (int) (remainingSpace / barCount);
float y = pageSize.getHeight() - marginTop;
float x = marginLeft;
// The bottom of the tab occurs every staffHeight + barGap pixels.
for (int i = 0; i < barCount; i++) {
// Left bar; connects staff to tab
canvas.moveTo(x, y);
canvas.lineTo(x, y - staffHeight);
// Four measure separators, evenly distributed
for (int j = 1; j < 4; j++) {
canvas.moveTo(x + clefWidth + (measureWidth * j), y);
canvas.lineTo(x + clefWidth + (measureWidth * j), y - (staffGap * 4));
canvas.moveTo(x + clefWidth + (measureWidth * j), y - ((staffGap * 5) + separatorGap));
canvas.lineTo(x + clefWidth + (measureWidth * j), y - staffHeight);
}
// Draw the last one exactly on the end
canvas.moveTo(x + staffWidth, y);
canvas.lineTo(x + staffWidth, y - (staffGap * 4));
canvas.moveTo(x + staffWidth, y - ((staffGap * 5) + separatorGap));
canvas.lineTo(x + staffWidth, y - staffHeight);
for (int j = 0; j < 5; j++) {
canvas.moveTo(x, y);
canvas.lineTo(x + staffWidth, y);
y -= staffGap; // I move y just a hair too far here. Rather than back up, I just account
// for it in the multiplications above
}
y -= separatorGap;
for (int j = 0; j < 6; j++) {
canvas.moveTo(x, y);
canvas.lineTo(x + staffWidth, y);
y -= tabGap;
}
y -= barGap;
y -= additionalBarGap;
}
canvas.stroke();
// Now start outputting the tablature fingerings
Note[][][] measures = new Note[][][] {
// Opening riff of Sweet Child O' Mine
new Note[][] {
new Note[] {new Note(GuitarString.D, 12, Duration.eighth)},
new Note[] {new Note(GuitarString.B, 14, Duration.eighth)},
new Note[] {new Note(GuitarString.B, 12, Duration.eighth)},
new Note[] {new Note(GuitarString.D, 12, Duration.eighth)},
new Note[] {new Note(GuitarString.e, 15, Duration.eighth)},
new Note[] {new Note(GuitarString.G, 13, Duration.eighth)},
new Note[] {new Note(GuitarString.e, 14, Duration.eighth)},
new Note[] {new Note(GuitarString.G, 13, Duration.eighth)}
}
};
canvas.beginText();
BaseFont bf = BaseFont.createFont(BaseFont.COURIER, BaseFont.CP1252, BaseFont.NOT_EMBEDDED);
canvas.setFontAndSize(bf, 8.0F);
// Move to the bottom of the first staff, left-most position (allowing a little room
// for the treble clef.
y = pageSize.getHeight() - marginTop - staffHeight;
x = marginLeft + clefWidth;
int measureCount = 0;
for (Note[][] measure : measures) {
for (Note[] chord : measure) {
for (Note note : chord) {
note.renderTab(canvas, x, y);
note.renderNote(canvas, x, y + staffHeight - (staffGap * 8));
}
x += noteWidth;
}
x = marginLeft + clefWidth + (measureWidth * ++measureCount) + 5.0f;
if (measureCount == 4) {
y = y - barGap - staffHeight - tabGap - additionalBarGap;
x = marginLeft + clefWidth;
measureCount = 0;
}
}
canvas.endText();
document.close();
}
}
The output is illustrated in figure 3, or you can download The full PDF. The clunkiest part is the "flags" on the notes; I just represent these with simple rectangles rather than trying to compute bezier curves and fill those in. Still, the output is quite an improvement over the ASCII-art tab charts you find all over the internet.
There's a lot missing from this iteration. Probably the most jarring for a professional musician is the absence of key signatures or flatted notes. I also space out the notes horizontally in equal measures, which isn't right — a half note should take up half of the horizontal space. Rest notations (hats and other symbols that indicate that you should not play for a particular duration) are conspicuously absent as well. Of course, all of the decorations like the 4/4 marker, the treble clef symbol, repetition bars, etc. don't appear in this rendition either. A lot of (but not all of) these have Unicode characters, but don't appear in every font on every implementation — I'd be better off in most cases importing graphics rather than relying on font support.
Most of these are just a matter of implementation, but one thing that really makes music notation look professional is the joining together of "related" flags on eighth notes called beams, which would be much more difficult to achieve.
But I must admit that the piece that needs the most work is the input interface! Right now, the only way to generate a new bit of tablature is to update the code; there should be, at a minimum, an input file to convert from. I don't do any error or sanity checking; if you try to stick 100 whole notes on the 57th fret of the high e-string, I'll do my best to render it. But, as difficult as it is for a command line fanatic like myself to admit, this is really the sort of thing that could really benefit from a GUI. That would be a pretty big refactoring, though, because the PDF interface isn't quite the same as, say the Swing or SWT interface; all output should be routed through a common interface so that the same display can be rendered on a print-ready PDF as easily as on a screen.