Writing Music in Java: Two Approaches
By Lance Finney, OCI Senior Software Engineer
January 2008
Introduction
Music software enables expressing musical ideas that must be both human-readable and computer-readable. Modern sheet music notation is extremely expressive, with the ability to communicate rhythm, melody, harmony, and a variety of performance instructions in a compact space. Unfortunately, as a graphical, human-readable notation, sheet music doesn't translate to computers well. A separate notation system, a Domain Specific Language (DSL), is necessary for computers to be able to process music. Also, we need tools that understand this DSL and allow us to manipulate the music.
This article examines two open source Java libraries that use two different notations which express musical information in computer-friendly ASCII formats. Both libraries can play tunes as MIDI sequences through the computer speakers but differ in their other capabilities.
JFugue
JFugue is an LGPL-licensed open source library for "programming music without the complexities of MIDI." It has its own notation to represent music using only ASCII characters, provides I/O to MIDI files, and allows manipulating music programmatically.
To demonstrate the capabilities of JFugue, we will use variations on the nursery rhyme "Itsy Bitsy Spider."
Simple Example: a Nursery Rhyme
The first demonstration of JFugue shows mainly the basic notation for the melody of the song:
- package com.ociweb.jnb.jfugue;
-
-
- import org.jfugue.Player;
-
-
- /**
- * This program plays a simple version of "Itsy Bitsy Spider." Though not
- * specified, the song is in 6/8 time and in the key of F major. The song
- * plays only the melody, with code duplication.
- */
- public class ItsyBitsySimple {
- public static void main(String[] args) {
- Player player = new Player();
- player.play(
- // "Itsy, bitsy spider, climbed up the water spout."
- "F5q F5i F5q G5i A5q. A5q A5i G5q F5i G5q A5i F5q. Rq. " +
- // "Down came the rain and washed the spider out."
- "A5q. A5q Bb5i C6q. C6q. Bb5q A5i Bb5q C6i A5q. Rq. " +
- // "Out came the sun and dried up all the rain, so the"
- "F5q. F5q G5i A5q. A5q. G5q F5i G5q A5i F5q. C5q C5i " +
- // "itsy, bitsy spider went up the spout again."
- "F5q F5i F5q G5i A5q. A5q A5i G5q F5i G5q A5i F5q. Rq."
- );
- }
- }
The primary lesson here is in the notation used to define the song. This simple example includes note names, octaves, rests, accidentals, and durations.
Notes, Octaves, and Rests
Notes are specified according to the simple A-G scale with the octave number specified next. For example, middle C is C5, the C an octave higher is C6, and the note directly below that one is B5. This is a common numbering system used in some instruments like handbells.
If no octave is given, the default octave for notes is octave 5, at and above middle C.
JFugue also allows specifying the note as a number from 0 to 127, or even defining pitches in between the notes (for some non-Western musical traditions and some types of modern music), but that advanced detail is outside the scope of this document.
Rests are defined using an R instead of a note-octave combination.
Accidentals
To specify sharps or flats, add a #
or b
between the note and the octave. Our example version of "Itsy Bitsy Spider" is in the key of F major, but JFugue defaults to C major (like standard musical notation), so we need to specify that the Bs in the second line are really B-flats. Later, we will see that we can specify the key signature for the song, so we won't need to specify sharps and flats that are within the key signature. Additionally, naturals can be denoted using an n
to cancel accidentals in keys other than C major (key signatures are covered later). Double-sharps and double-flats are not supported.
Durations
The simplest means of expressing note duration is the one used here, based on the American system:
Code | Duration |
w | whole note |
h | half note |
q | quarter note |
i | eighth note |
s | sixteenth note |
t | thirty-second note |
x | sixty-fourth note |
n | one-twenty-eighth note |
In the example song above, we use quarter notes (q
), eighth notes (i
), and dotted-quarter notes (q.
). As in standard musical notation, adding a dot after a note extends its length by 50%.
Other durations, such as triplets and other tuplets can be defined by an alternate numerical notation based on the whole note. For example, to define a C5 that is a quarter note, use C5/0.25. To define a C5 note that is a third the length of a quarter note, use C5/0.08333333. As we will see later, this notation is also used when importing a MIDI file into JFugue notation.
Playing MIDI
In addition to showing the notation, this example shows a bit of JFugue's API. Specifically, the call to player.play()
converts the defined song to a MIDI sequence and plays it through the computer's speakers.
Adding Measures, Patterns, and Voices
Now that the basics of the musical notation have been defined, we can start to improve the song. First, from a DRY perspective, we should get rid of the duplication between the first and the last line, which are identical musically. Fortunately, JFugue provides a Pattern
class that uses the Composite design pattern and allows us to reuse musical segments. In the following version, we create a Pattern
instance for each unique line and then add each of them in order to another Pattern
instance that represents the entire song.
- package com.ociweb.jnb.jfugue;
-
-
- import org.jfugue.Pattern;
- import org.jfugue.Player;
-
-
- /**
- * This program plays the version of "Itsy Bitsy Spider" that is the basis of
- * later examples in the article. Though not specified, the song is in 6/8
- * time and in the key of F major. The song plays only the melody, with code
- * duplication eliminated and with measures indicated.
- */
- public class ItsyBitsy {
- public static void main(String[] args) {
- // "Itsy, bitsy spider, climbed up the water spout."
- // and "itsy, bitsy spider went up the spout again."
- Pattern pattern1 = new Pattern("F5q F5i F5q G5i | A5q. A5q A5i | G5q F5i G5q A5i | F5q. Rq. | ");
- // "Down came the rain and washed the spider out."
- Pattern pattern2 = new Pattern("A5q. A5q Bb5i | C6q. C6q. | Bb5q A5i Bb5q C6i | A5q. Rq. | ");
- // "Out came the sun and dried up all the rain, so the"
- Pattern pattern3 = new Pattern("F5q. F5q G5i | A5q. A5q. | G5q F5i G5q A5i | F5q. C5q C5i | ");
-
-
- // Put the whole song together
- Pattern song = new Pattern();
- song.add(pattern1);
- song.add(pattern2);
- song.add(pattern3);
- song.add(pattern1);
-
-
- // Play the song
- Player player = new Player();
- player.play(song);
- }
- }
Another change here is that we have added markers for the boundaries between measures (the "|" characters). Interestingly, this has no effect on the program's interpretation of the song - it's for user convenience and clarity only. JFugue does not have the concept of a time signature, so measures may contain as many or as few beats as you like - they are there only for reading convenience.
Next, let use the Voice feature to create a round.
- package com.ociweb.jnb.jfugue;
-
-
- import org.jfugue.Pattern;
- import org.jfugue.Player;
-
-
- /**
- * This program plays "Itsy Bitsy Spider" as a round. Though not specified,
- * the song is in 6/8 time and in the key of F major. The song plays the
- * melody in three repetitions.
- */
- public class ItsyBitsyRound {
- public static void main(String[] args) {
- // "Itsy, bitsy spider, climbed up the water spout."
- // and "itsy, bitsy spider went up the spout again."
- Pattern pattern1 = new Pattern("F5q F5i F5q G5i | A5q. A5q A5i | G5q F5i G5q A5i | F5q. Rq. | ");
- // "Down came the rain and washed the spider out."
- Pattern pattern2 = new Pattern("A5q. A5q Bb5i | C6q. C6q. | Bb5q A5i Bb5q C6i | A5q. Rq. | ");
- // "Out came the sun and dried up all the rain, so the"
- Pattern pattern3 = new Pattern("F5q. F5q G5i | A5q. A5q. | G5q F5i G5q A5i | F5q. C5q C5i | ");
-
-
- // Put the whole song together
- Pattern song = new Pattern();
- song.add(pattern1);
- song.add(pattern2);
- song.add(pattern3);
- song.add(pattern1);
-
-
- Pattern lineRest = new Pattern("Rh. | Rh. | Rh. | Rh. | ");
-
-
- // Create the first voice
- Pattern round1 = new Pattern("V0");
- round1.add(song);
-
-
- // Create the second voice
- Pattern round2 = new Pattern("V1");
- round2.add(lineRest);
- round2.add(song);
-
-
- // Create the third voice
- Pattern round3 = new Pattern("V2");
- round3.add(lineRest, 2);
- round3.add(song);
-
-
- // Put the voices together
- Pattern roundSong = new Pattern();
- roundSong.add(round1);
- roundSong.add(round2);
- roundSong.add(round3);
-
-
- // Play the song
- Player player = new Player();
- player.play(roundSong);
- }
- }
In this example, we create a round by combining three similar Voices (similar to tracks or channels in other musical contexts). In this case, there is a separate Pattern
instance for each Voice. Each pattern receives the same sequence from the previous example, but some are prefixed with one or more full lines of rests (lineRest
) so that they have staggered starts.
The separate voices are defined by adding V0
, V1
, and V2
to the pattern. Everything after the Voice declaration is associated with that Voice, until another Voice is specified. In this example, all of the information for each Voice is grouped together, but the definitions could be interspersed as long as the Voice is respecified each time, as shown in the following version that uses the Pattern
instances introduced earlier. This example is musically identical to the preceding example.
- Pattern lineRest = new Pattern("Rh. | Rh. | Rh. | Rh. | ");
-
-
- // Put the whole song together
- Pattern song = new Pattern();
- song.add("V0 " + pattern1);
- song.add("V1 " + lineRest);
- song.add("V2 " + lineRest);
-
-
- song.add("V0 " + pattern2);
- song.add("V1 " + pattern1);
- song.add("V2 " + lineRest);
-
-
- song.add("V0 " + pattern3);
- song.add("V1 " + pattern2);
- song.add("V2 " + pattern1);
-
-
- song.add("V0 " + pattern1);
- song.add("V1 " + pattern3);
- song.add("V2 " + pattern2);
-
-
- song.add("V1 " + pattern1);
- song.add("V2 " + pattern3);
-
-
- song.add("V2 " + pattern1);
-
-
- // Play the song
- Player player = new Player();
- player.play(song);
-
Adding Chords, Instruments, Key Signatures, and Tempo
Another use of Voices is to add harmonies or chord accompaniments. In the following example, V0
is used as the melody introduced earlier, and V1
is used to provide bass chords. Note that only the short name of the chord need be specified (Fmaj
and Bbmaj
in this example, but there are many other options for more complicated chords), and that the default octave is number 3 (two octaves below middle C). Chords can also be defined by specifying each of the notes in the chord, connected with +
. In this example, we use this approach for one of the chords to use the first inversion of the chord, which does not have a short name.
Another addition to this version is instrumentation. The default instrument for all Voices is the Piano, so the previous examples sounded like they were played on a piano. In this example, the melody is a trumpet (specified as I[Trumpet]
or alternately as I56
), and the chords are a church organ (specified as I[CHURCH_ORGAN]
or alternately as I19
). The instruments are defined here in the header, but they also can be changed at any point during the song.
The ID numbers and options here are derived from the MIDI specification; the full list of 128 instruments is available in JFugue's documentation.
- package com.ociweb.jnb.jfugue;
-
-
- import org.jfugue.Pattern;
- import org.jfugue.Player;
-
-
- /**
- * This program plays "Itsy Bitsy Spider" in two voices. The first voice is a
- * melody is played as a trumpet, and the second (new) voice is a church organ
- * playing chords. Though not specified, the song is in 6/8 time and in the
- * key of F major.
- */
- public class ItsyBitsyChords {
- public static void main(String[] args) {
- Pattern voice1 = new Pattern("V0 I[Trumpet] ");
- // "Itsy, bitsy spider, climbed up the water spout."
- // and "itsy, bitsy spider went up the spout again."
- Pattern pattern1 = new Pattern("V0 F5q F5i F5q G5i | A5q. A5q A5i | G5q F5i G5q A5i | F5q. Rq. | ");
- // "Down came the rain and washed the spider out."
- Pattern pattern2 = new Pattern("V0 A5q. A5q Bb5i | C6q. C6q. | Bb5q A5i Bb5q C6i | A5q. Rq. | ");
- // "Out came the sun and dried up all the rain, so the"
- Pattern pattern3 = new Pattern("V0 F5q. F5q G5i | A5q. A5q. | G5q F5i G5q A5i | F5q. C5q C5i | ");
-
-
- Pattern voice2 = new Pattern("V1 I[CHURCH_ORGAN] ");
- //1st, 3rd, and 4th lines (third chord specified as notes)
- Pattern chord1 = new Pattern("V1 Fmajh. | Fmajh. | E3h.+G3h.+C4h. | Fmajh. | ");
- //2nd line
- Pattern chord2 = new Pattern("V1 Fmajh. | Fmajh. | Bbmajh. | Fmajh. | ");
-
- // Put the whole song together
- Pattern song = new Pattern();
-
- //melody
- song.add(voice1);
- song.add(pattern1);
- song.add(pattern2);
- song.add(pattern3);
- song.add(pattern1);
-
-
- //chords
- song.add(voice2);
- song.add(chord1);
- song.add(chord2);
- song.add(chord1, 2);
-
-
- // Play the song
- Player player = new Player();
- player.play(song);
- }
- }
The last JFugue version of "Itsy Bitsy Spider" in adds two elements to the header: the key signature and the tempo. These elements can be defined anywhere in the course of a song to change keys or tempo (to express a ritardano, for example), but our example sets them only intitially. If we were to change the tempo during the course of the song, we would have to change it separately for each voice.
The key signature is specified as F major (KFmaj
), which means that the flat notations can removed from the B notes, just as they could be in standard musical notation. As mentioned before, the default key signature is C major.
The tempo is specified as 100 "Pulses Per Quarter," which is how many "pulses" to give a quarter note. The JFugue documentation is actually fairly confusing on tempo, because the default value is 120, which the documentation simultaneously defines as 120 "pulses" per quarter note and 120 beats per minute. The two scales act in different directions, in that more pulses per quarter would be slower, but more beats per minute would be faster. In fact, the first definition is the one in use here, and T100
is faster than the default tempo.
- package com.ociweb.jnb.jfugue;
-
-
- import org.jfugue.Pattern;
- import org.jfugue.Player;
-
-
- import java.io.File;
-
-
- /**
- * This program plays "Itsy Bitsy Spider" in two voices. The first voice is a
- * melody is played as a trumpet, and the second (new) voice is a church organ
- * playing chords. The key of F major and a tempo are specified. Though not
- * specified, the song is in 6/8 time.
- */
- public class ItsyBitsyHeader {
- public static void main(String[] args) throws IOException {
- Pattern header = new Pattern("KFmaj T100 V0 I[Trumpet] V1 I[CHURCH_ORGAN] ");
- // "Itsy, bitsy spider, climbed up the water spout."
- // and "itsy, bitsy spider went up the spout again."
- Pattern pattern1 = new Pattern("V0 F5q F5i F5q G5i | A5q. A5q A5i | G5q F5i G5q A5i | F5q. Rq. | ");
- // "Down came the rain and washed the spider out."
- Pattern pattern2 = new Pattern("V0 A5q. A5q B5i | C6q. C6q. | B5q A5i B5q C6i | A5q. Rq. | ");
- // "Out came the sun and dried up all the rain, so the"
- Pattern pattern3 = new Pattern("V0 F5q. F5q G5i | A5q. A5q. | G5q F5i G5q A5i | F5q. C5q C5i | ");
-
-
- //1st, 3rd, and 4th lines (third chord specified as notes)
- Pattern chord1 = new Pattern("V1 Fmajh. | Fmajh. | E3h.+G3h.+C4h. | Fmajh. | ");
- //2nd line
- Pattern chord2 = new Pattern("V1 Fmajh. | Fmajh. | Bmajh. | Fmajh. | ");
-
- // Put the whole song together
- Pattern song = new Pattern();
- song.add(header);
-
- //melody
- song.add(pattern1);
- song.add(pattern2);
- song.add(pattern3);
- song.add(pattern1);
-
-
- //chords
- song.add(chord1);
- song.add(chord2);
- song.add(chord1, 2);
-
-
- // Play the song
- Player player = new Player();
- player.play(song);
-
- // save as a midi file for use in the next example
- player.saveMidi(song, new File("spider.midi"));
- }
- }
Loading, Saving, and Manipulating MIDI Files
In addition to playing a song to the speakers as a MIDI sequence, as the previous examples did, we can also load a MIDI file into the JFugue library and/or export a JFugue song to a MIDI file. Additionally, JFugue provides an API for musical transformations to be applied to a Pattern
, with a few transformations implemented in the library.
In the following example, a MIDI file created using the final version of the song is loaded in, and the parsed JFugue representation is printed to the command line. Then, the entire song is transposed up a whole note using a IntervalPatternTransformer
and reprinted. Next, the entire song is slowed down 20% using a DurationPatternTransformer
and reprinted again. Finally, the newly modified song is exported back to a new MIDI file.
- package com.ociweb.jnb.jfugue;
-
-
- import org.jfugue.Pattern;
- import org.jfugue.Player;
- import org.jfugue.extras.DurationPatternTransformer;
- import org.jfugue.extras.IntervalPatternTransformer;
-
-
- import javax.sound.midi.InvalidMidiDataException;
- import java.io.File;
- import java.io.IOException;
-
-
- /**
- * This program demonstrates MIDI I/O and musical transformations.
- */
- public class IOTransformations {
- public static void main(String[] args) throws InvalidMidiDataException, IOException {
- Player player = new Player();
- // load a midi file
- Pattern pattern = player.loadMidi(new File("spider.midi"));
- // print the song to the console with JFugue notation
- System.out.println("Original: " + pattern.getMusicString());
-
- // transpose up a whole note
- IntervalPatternTransformer transposer = new IntervalPatternTransformer();
- transposer.putParameter(IntervalPatternTransformer.INTERVAL, 2);
- pattern = transposer.transform(pattern);
- System.out.println("Transposed: " + pattern.getMusicString());
-
- // slow down 20%
- DurationPatternTransformer slower = new DurationPatternTransformer(1.2);
- pattern = slower.transform(pattern);
- System.out.println("Slowed: " + pattern.getMusicString());
-
- // save as a midi file
- player.saveMidi(pattern, new File("output.midi"));
- }
- }
Here is the output (trimmed for line length):
Original: V0 @0 V0 I[Trumpet] @0 V0 F5/0.20833333333333334 @100 V0 F5/0.10416666666666667...
Transposed: V0 @0 V0 I[Trumpet] @0 V0 G5/0.20833333333333334 @100 V0 G5/0.10416666666666667...
Slowed: V0 @0 V0 I[Trumpet] @0 V0 G5q @100 V0 G5i...
Note that the original version uses the numeric notation for note duration because a non-default tempo was used. In the last version, when the tempo is changed exactly to the default notation, the simplified notation can be used.
Also note that the transposition transformation moved the notes up from F5
to G5
.
Extensions
An interesting project is underway to create a GUI frontend for JFugue. Geertjan Wielenga has started JFugue Music NotePad, a Netbeans-based project that allows users to create simple tunes by clicking on a staff to place notes and rests.
Currently, JFugue Music NotePad supports only a subset of the overall facilities of JFugue (only a single voice, no chords, only the basic note durations, no input of MIDI or JFugue files), but it plays the tunes created in it, allows printing a view of the music, and exports the tune to a MIDI file. So, it's not a tool that could be used to create a complicated musical piece, but it is an intriguing project that might be useful for teaching musical ideas.
Figure 1: JFugue Music NotePad showing "Mary Had a Little Lamb"
ABC4J
JFugue has a musical notation that is fairly complete, but one disadvantage is that it is not a standard; few if any other applications use the same notation, and libraries of songs that use the notation are hard to find. A competing notation that is more commonly used is abc notation. Abc notation was created in 1991 as a means of sharing folk songs and traditional tunes using only ASCII characters. It has become very popular in certain communities, and collections of tunes using the notation can be found on the Web. Additionally, the notation is a rigorously defined language, with a BNF definition.
On the downside, abc notation is not as complete musically as JFugue's notation. For example, it does not support multiple voices (at least, not in the formal language — some applications have their own extensions), and it does not support defining instruments. These limitations make sense given the original purpose of the language: to share the melodies of historical tunes.
There is a nascent GPL-licensed project to build a Java library that uses abc notation: abc4j. The API is less mature than JFugue's, and there is less functionality (no MIDI input or output, no ability to transform the tunes, etc.), but it provides an alternative for those who wish to use the large collections of music already expressed in abc notation.
Writing "Itsy Bitsy Spider" with abc4j
Unlike JFugue, abc4j does not have a simple means of defining the tune with a DSL within a program. The two options are either to import the song from a file or to use an extremely verbose API (i.e. score.addElement(new Note(Note.C))
). Below is an example of abc notation for "Itsy Bitsy Spider" that is loaded into a program and played.
The music file:
X:1
T:Simple Itsy Bitsy
M:6/8
L:1/8
O:nursery rhyme
Q:240
K:F
[| F2F F2G | A3 A2A | G2F G2A | F3 z3 |\
A3 A2B | c3 c3 | B2A B2c | A3 z3 |\
F3 F2G | A3 A3 | G2F G2A | F3 C2C |\
F2F F2G | A3 A2A | G2F G2A | F3 z3 |]
The header of an abc file allows specifying a lot more metadata than JFugue supports. Here, we define in order:
- Reference number (a file can contain multiple tunes)
- Title
- Time signature
- Default note length (here, it's an eighth note) - it means that other durations are specified in terms of this note.
- Origin
- Tempo in default notes per minute
- Key: F major
After the header, the actual tune is displayed. There are several aspects seen here that are different than in JFugue's notation:
- Octaves are specified using shorthand notation instead of numbered notation.
- The number given after a note is the duration in terms of the default note length (so, F2 means a quarter note at F, since the default note length for this tune is an eighth note).
- Rests are specified using
z
instead ofR
. - Multiple voices are not supported.
- Special notation exists for the beginning of the tune (
[|
), the end of the tune (|]
), and continuing a line (\
). There is also support for repeats, though this example does not use it.
Below is a sample programs which loads the above file, examines the metadata, exports the file, plays the tune, and displays a representation of the score.
- package com.ociweb.jnb.abc4j;
-
-
- import abc.midi.TunePlayer;
- import abc.notation.Tune;
- import abc.parser.TuneBook;
- import abc.ui.swing.JScoreComponent;
-
-
- import javax.swing.JFrame;
- import java.io.File;
- import java.io.IOException;
-
-
- /**
- * This program demonstrates abc4j's capabilities. It loads a song from a
- * file, displays metadata, saves the song to a different file, plays the song,
- * and then displays a representation of the score.
- */
- public class ItsyBitsyAbc {
- public static void main(String[] args) throws IOException {
- //loading from the file
- TuneBook book = new TuneBook(new File("itsyBitsy.abc"));
-
-
- // show details about the tunes that are loaded
- System.out.println("# of tunes in itsyBitsy.abc : " + book.size());
-
- // retrieve the specific tune by reference number
- Tune tune = book.getTune(1);
-
-
- // display its title
- System.out.print("Title of #1 is " + tune.getTitles()[0]);
- // and its key
- System.out.println(" and is in the key of " + tune.getKey().toLitteralNotation());
-
- // can export to a file (abc notation)
- book.saveTo(new File("out.abc"));
-
-
- // creates a simple midi player to play the melody
- TunePlayer player = new TunePlayer();
- player.start();
- player.play(tune);
-
-
- // creates a component that draws the melody on a musical staff
- JScoreComponent jscore = new JScoreComponent();
- jscore.setJustification(true);
- jscore.setTune(tune);
- JFrame j = new JFrame();
- j.add(jscore);
- j.pack();
- j.setVisible(true);
- // writes the score to a JPG file
- jscore.writeScoreTo(new File("spiderScore.jpg"));
- }
- }
Figure 2: JScore output from abc4j
The access to metadata here (number of songs in the file and the name and key of the tune) is an advantage over what is available in JFugue. Another minor advantage is the ability to print a score of the tune without needing an external GUI application. However, we cannot transform the tune here, we hear and process only one voice, and there is no direct access to MIDI files.
Summary
Neither JFugue notation nor abc notation is as expressive as the standard staff-based graphical score, but both allow computer-based sharing and modification of songs. Each notation supports some musical ideas that the other doesn't support, but JFugue notation is closer to complete. Additionally, the JFugue library is much more powerful than the relatively new abc4j library. However, until an abc notation parser is added to JFugue, abc4j allows access to a much larger collection of music from around the world.
The best way to become familiar with these tools is to try them out yourself. Starting with the examples here, you can easily tweak them and create interesting music using either notation (both of which allow for musical expression and concepts beyond the scope of this article). Another way to start is to download a MIDI file from a Web repository and import it into JFugue's notation; seeing how a symphony by Beethoven or a song by the Beatles is encoded effectively demonstrates the power of the notation.
References
- [1] JFugue (version 3.2 used in this article)
http://jfugue.org/ - [2] JFugue Music NotePad
http://www.jfugue.org/ - [3] abc4j (version 0.3 used in this article)
http://code.google.com/p/abc4j/ - [4] abc music notation
http://www.walshaw.plus.com/abc/
Lance Finney thanks Michael Easter, Tom Wheeler, and Dean Wette for reviewing this article and providing useful suggestions.