Convert TempleOS hymns to BSD speaker(4) format

A note about Terry A. Davis (1969–2018).

Terry A. Davis was the creator of TempleOS, which included these ‘hymns’, sometimes called “TAD Hymns”, though I think not by himself directly. Davis was surely a brilliant computer engineer; but he was also known to make racist and homophobic remarks. This page isn’t meant to be an endorsement of those sentiments. My only goal is to explore the tunes of these hymns – not the text – and to make them playable on a PC speaker without loading TempleOS.

Table of contents:

A bit of background

The goal: convert TempleOS hymns into a format accepted by the BSD speaker(4) device. But what are TempleOS hymns, and what is a BSD speaker(4) device?

What are TempleOS hymns?

The best way to explain a TempleOS hymn is for you to see an example for yourself. If you have a TempleOS ISO and a real PC with a real PC speaker, you could just boot TempleOS, load up the Jukebox, and play the hymns for yourself.

Or, you could watch this YouTube video:

If memory serves, Davis made this recording with an old version of VMware Workstation which still had PC speaker support and, from the sounds of it, did some post-processing to add an echo and maybe some reverb. So this will sound rather different if you play it on a real PC speaker.

Most of the original YouTube videos are gone now, but some archives exist, such as this channel offering this video.

I can’t remember what format these hymns were in originally, though it wouldn’t surprise me if they were written in HolyC, the language for the compiler Davis wrote for TempleOS. My source for the hymns is TAD Hymns (MIDI) on the Internet Archive. I didn’t do any investigation into how these were created, or how accurate they are, but they do sound correct to me, once they have been patched – more on that later.

What is the BSD speaker(4) device?

This is a UNIX device, usually /dev/speaker, which accepts a string input of pitches, durations, etc, and causes the PC speaker to emit them accordingly. In short, it plays single-voice music. Sources indicate it was originally written in the early ’90s by Eric S. Raymond, and is now found in (at least) FreeBSD and NetBSD.

It’s based on the PLAY statement (and its “tune definition language”) found in IBM BASIC 2.0. The IBM BASIC Reference – PLAY Statement describes the original syntax, which has extra features not available in speaker(4) and lacks some newer features introduced into speaker(4).

Here’s an example of how it’s used:

# echo 'O3L8AB->DCL16<B-AL8GCFGAL16B-AG8' >> /dev/speaker
# 

If all went well, the only output you’ll receive are the dulcet tones of the PC speaker serenading you. If you didn’t hear that, check that you actually have a device at /dev/speaker and that you didn’t just create a text file there. If you do have a UNIX device, poke your head into your computer case and see whether you actually do have a PC speaker in there.

All of these characters are well-described in the (rather good) manpage, but briefly: it first sets the octave (O3), then sets the note length to an eighth note, or semiquaver (L8), plays some notes (A, then B-), etc.

On my system, the total size seems limited to less than 512 bytes at a time – this is a possibility mentioned in the manpage. Of course, you can just send multiple lines, or play tricks with dd(1) to send data in smaller chunks.

Converting a simple MIDI file

Before attempting to convert TempleOS hymn MIDI files – which are a bit nonstandard – it seems prudent to try a simple, well-formed example first.

Here’s an example in LilyPond which will produce a useful test:

$ cat demo.ly
\version "2.18.2"
\score {
  \relative c' {
    d2 fis4 e8 g16 fis32 a64
  }
  \midi { \tempo 4 = 120 }
}
$ lilypond demo.ly
GNU LilyPond 2.18.2
Processing `demo.ly'
Parsing...
Interpreting music...
MIDI output to `demo.midi'...
Success: compilation successfully completed
$ file demo.midi
demo.midi: Standard MIDI data (format 1) using 2 tracks at 1/384

Note that because I didn’t include a \layout { } block, no PDF will be created; only the MIDI output is produced. (The music is rather nonsensical anyhow.)

Using midicsv to examine the MIDI file

John Walker, founder of Autodesk (amongst other notable achievements), released an excellent utility called midicsv. As its name implies, it takes a binary MIDI file and produces a text CSV file representation of it. It can also do the inverse, using csvmidi.

Let’s see what it produces for the above demo.midi file; I’m grepping out some of the extra text LilyPond stuffs in there for the sake of brevity:

$ midicsv demo.midi | grep -v '_t, '
0, 0, Header, 1, 2, 384
1, 0, Start_track
1, 0, Time_signature, 4, 2, 18, 8
1, 0, Tempo, 500000
1, 0, End_track
2, 0, Start_track
2, 0, Control_c, 0, 7, 100
2, 0, Note_on_c, 0, 62, 90
2, 768, Note_on_c, 0, 62, 0
2, 768, Note_on_c, 0, 66, 90
2, 1152, Note_on_c, 0, 66, 0
2, 1152, Note_on_c, 0, 64, 90
2, 1344, Note_on_c, 0, 64, 0
2, 1344, Note_on_c, 0, 67, 90
2, 1440, Note_on_c, 0, 67, 0
2, 1440, Note_on_c, 0, 66, 90
2, 1488, Note_on_c, 0, 66, 0
2, 1488, Note_on_c, 0, 69, 90
2, 1512, Note_on_c, 0, 69, 0
2, 1512, End_track
0, 0, End_of_file

The general format is: track, time, event[, event_data, …].

A few fields and events explained:

Calculating note duration

Looking at the above, you may well ask, “Where is the note duration stored?” The short answer is: it’s not.

Note durations need to be calculated by looking at the time the note started, and the time the note stopped, then taking the difference between those times.

If you consider the event-driven nature of MIDI, it actually makes sense; if notes were sent as single events with durations rather than as on–off event pairs, the device receiving these MIDI events would have to ‘remember’ each note and how long it was supposed to stay on, then turn it off at the appropriate time. Fine for a few notes, but not very efficient when dealing with many notes at once, considering that, at each tick, the list of active notes would have to be checked to see which need to be stopped.

Calculating note pitch

In General MIDI, note 60 corresponds to middle C, and all other notes are relative to this. Note values increment in semitones.

In speaker(4), octave 3 (O3) starts with middle C; thus O3C is middle C. The default octave appears to be 4, but it’s probably best to explicitly specify the desired octave at the outset, unless you’re exclusively using absolute pitches.

There is also a way in speaker(4) to specify note values absolutely using the N command. Surprisingly the manpage doesn’t give the number for middle C, but you can easily prove that it is 37:

# echo 'O3CN37' >> /dev/speaker

That should play the same note twice. Thus, to get a speaker(4) absolute note number, subtract 23 from the MIDI note number.

Putting it all together

Now the demo file can be translated in its entirety:

StartEndDurationPitchspeaker(4) string
TicksLnnMIDINnn[On] note
[ [ "0", "768", "768", "L2", "62", "N39", "O3D", "O3D2" ], [ "768", "1152", "384", "L4", "66", "N43", "F#", "F#4" ], [ "1152", "1344", "192", "L8", "64", "N41", "E", "E8" ], [ "1344", "1440", "96", "L16", "67", "N44", "G", "G16" ], [ "1440", "1488", "48", "L32", "66", "N43", "F#", "F#32" ], [ "1488", "1512", "24", "L64", "69", "N46", "A", "A64" ] ]

Which gives:

O3D2F#4E8G16F#32A64

How these fields were calculated:

It’s a bit laborious to do this by hand, although the calculations are well-suited to a spreadsheet program, which is how I made these tables.

A brief interlude about quantization

Looking at the demo example above, the duration (in ticks) of the music is rather nice & neat; every duration converted perfectly into an L-value. This will likely be true of most computer-generated MIDI files that don’t involve a human touching an instrument.

In the “real world”, things are not so simple. Humans don’t play instruments in a beat with the precision of one MIDI clock interval, nor would this be desirable. So there has to be a way to take these ‘messy’ durations and “straighten them out” into some semblance of order. That process is called quantization.

The limits in this case are the limits of our output language, verified by examining the manpages and source code:

There are probably some very clever ways to quantize MIDI data so that it fits neatly into these restrictions; that’s not what I’m going to do. I’m not much of a mathematician, or even a computer scientist; but I am a musician, so that is how I will approach this. Hopefully whatever error I introduce into the output is less than can be perceived by most humans; but if not, oh well.

Converting a TempleOS hymn: abiding

Time for a real-world example: let’s try to convert abiding.

abiding.mid

Let’s midicsv abiding.mid to see what’s inside:

$ midicsv abiding.mid | head -16
0, 0, Header, 0, 1, 384
1, 0, Start_track
1, 0, Tempo, 500000
1, 0, Time_signature, 4, 2, 24, 8
1, 0, Note_on_c, 0, 72, 64
1, 104, Note_on_c, 0, 77, 64
1, 209, Note_on_c, 0, 76, 64
1, 311, Note_on_c, 0, 74, 64
1, 387, Note_on_c, 0, 74, 64
1, 463, Note_on_c, 0, 74, 64
1, 539, Note_on_c, 0, 74, 64
1, 615, Note_on_c, 0, 74, 64
1, 691, Note_on_c, 0, 67, 64
1, 766, Note_on_c, 0, 74, 64
1, 842, Note_on_c, 0, 67, 64
1, 918, Note_on_c, 0, 72, 64

It starts out well enough, with the same quarter-note length (384 ticks) and tempo (120 BPM) as in the demo file before. But those note start times are definitely not aligned. Let’s try to calculate the durations…

…and then realize that there are no durations because there are never any note off events! Gah.

Sure enough, if you play this (for example, with Timidity) it sounds like a big, soupy mess. Listen to abiding-soup.flac to see what I mean.

It’s a minor annoyance, though, because it’s understood that this was meant to play on a PC speaker, which can only play one note at a time; therefore, just take the next note’s start time as the end of the currently-playing note, and assume there were no rests in the input. (I did eventually fix the MIDI files; see the Appendix for a copy of those.)

There is one problem with this approach, though: how long is the last note? Ordinarily it would be impossible to tell, but as most of these hymns are based on some repeating pattern, you can determine this by looking for the last time the particular pattern was repeated, and filling in the number by hand. (In the three hymns I have hand-converted so far, all final note durations were the same as the previous note.)

First (naïve) quantization attempt

I’ll attempt the same conversion as before on the first few notes – omitting pitch information for now – and see what happens:

StartEndDuration
TicksL
[ [ "0", "104", "104", "14.76923" ], [ "104", "209", "105", "14.62857" ], [ "209", "311", "102", "15.05882" ], [ "311", "387", "76", "20.21052" ], [ "387", "463", "76", "20.21052" ], [ "463", "539", "76", "20.21052" ], [ "539", "615", "76", "20.21052" ], [ "615", "691", "76", "20.21052" ], [ "691", "766", "75", "20.48" ], [ "766", "842", "76", "20.21052" ], [ "842", "918", "76", "20.21052" ] ]

(As a reminder, L was calculated as 4 × 384 ÷ ticks.)

While I was expecting a bit of deviation, considering the input, I didn’t expect the notes to be so far away from ‘usual’ values. (What is a 120 note anyhow?) I wonder if I can do better…

Note duration frequency analysis

Let’s do a frequency analysis on all of the note durations. I’ll treat anything ±3 as the same:

Duration(s)Count
[ [ "67 ", "1" ], [ "75 76 78", "46" ], [ "84 ", "1" ], [ "102 103 104 105", "23" ], [ "112 ", "1" ], [ "150 152 153 154 155", "23" ], [ "300 ", "1" ], [ "307 308 309 310", "31" ] ]

Maybe instead of using 384 as the “magic number” – which, although it exists in the MIDI file, also appears to have no basis in reality for the rest of the notes – I should use a different one. How about 76? It’s frequently used, multiples perfectly into another batch of frequently-used durations (152), and almost multiplies into the longest duration group (304 vs 307–310).

[ Later, using numerical error analysis, I determined that 77 would be very slightly better; but I was also rather happy to have stumbled upon nearly the best solution by accident. 38 was second-best, followed by 76. If anyone wants to know more about this part, I’ll be happy to share, though it’s even more mind-numbing than the rest of this stuff. ]

So how can one leverage this new number? I will show you the procedure I used, though it’s hardly formalized. (There are probably formal names for some of the steps I used, though I don’t know what they are.)

Second quantization attempt: success

Here is the table I used to produce the output; an explanation will follow below. Use the drop-down menu to choose how many rows you’d like to see:

Number of rows to display:
 
ABCDEFGHIJLMNO
MIDIendticks/76*3round64/round*3/4 = LN =O =notefinal
[ [ "72", "104", "104", "1.36842", "4.10526", "4", "16", "48", "12", "49", "4", "0", "C", "O4L12C" ], [ "77", "209", "105", "1.38157", "4.14473", "4", "16", "48", "12", "54", "4", "5", "F", "F" ], [ "76", "311", "102", "1.34210", "4.02631", "4", "16", "48", "12", "53", "4", "4", "E", "E" ], [ "74", "387", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "L16D" ], [ "74", "463", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "74", "539", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "74", "615", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "74", "691", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "67", "766", "75", "0.98684", "2.96052", "3", "21.33333", "64", "16", "44", "3", "7", "G", "<G" ], [ "74", "842", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", ">D" ], [ "67", "918", "76", "1", "3", "3", "21.33333", "64", "16", "44", "3", "7", "G", "<G" ], [ "72", "1225", "307", "4.03947", "12.11842", "12", "5.33333", "16", "4", "49", "4", "0", "C", ">L4C" ], [ "67", "1379", "154", "2.02631", "6.07894", "6", "10.66666", "32", "8", "44", "3", "7", "G", "<L8G" ], [ "76", "1533", "154", "2.02631", "6.07894", "6", "10.66666", "32", "8", "53", "4", "4", "E", ">E" ], [ "76", "1842", "309", "4.06578", "12.19736", "12", "5.33333", "16", "4", "53", "4", "4", "E", "L4E" ], [ "69", "1944", "102", "1.34210", "4.02631", "4", "16", "48", "12", "46", "3", "9", "A", "<L12A" ], [ "69", "2047", "103", "1.35526", "4.06578", "4", "16", "48", "12", "46", "3", "9", "A", "A" ], [ "77", "2159", "112", "1.47368", "4.42105", "4", "16", "48", "12", "54", "4", "5", "F", ">F" ], [ "67", "2459", "300", "3.94736", "11.84210", "12", "5.33333", "16", "4", "44", "3", "7", "G", "<L4G" ], [ "72", "2562", "103", "1.35526", "4.06578", "4", "16", "48", "12", "49", "4", "0", "C", ">L12C" ], [ "77", "2664", "102", "1.34210", "4.02631", "4", "16", "48", "12", "54", "4", "5", "F", "F" ], [ "76", "2766", "102", "1.34210", "4.02631", "4", "16", "48", "12", "53", "4", "4", "E", "E" ], [ "74", "2842", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "L16D" ], [ "74", "2918", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "74", "2996", "78", "1.02631", "3.07894", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "74", "3072", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "74", "3148", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "67", "3224", "76", "1", "3", "3", "21.33333", "64", "16", "44", "3", "7", "G", "<G" ], [ "74", "3300", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", ">D" ], [ "67", "3376", "76", "1", "3", "3", "21.33333", "64", "16", "44", "3", "7", "G", "<G" ], [ "72", "3685", "309", "4.06578", "12.19736", "12", "5.33333", "16", "4", "49", "4", "0", "C", ">L4C" ], [ "67", "3837", "152", "2", "6", "6", "10.66666", "32", "8", "44", "3", "7", "G", "<L8G" ], [ "76", "3990", "153", "2.01315", "6.03947", "6", "10.66666", "32", "8", "53", "4", "4", "E", ">E" ], [ "76", "4300", "310", "4.07894", "12.23684", "12", "5.33333", "16", "4", "53", "4", "4", "E", "L4E" ], [ "69", "4402", "102", "1.34210", "4.02631", "4", "16", "48", "12", "46", "3", "9", "A", "<L12A" ], [ "69", "4505", "103", "1.35526", "4.06578", "4", "16", "48", "12", "46", "3", "9", "A", "A" ], [ "77", "4607", "102", "1.34210", "4.02631", "4", "16", "48", "12", "54", "4", "5", "F", ">F" ], [ "67", "4916", "309", "4.06578", "12.19736", "12", "5.33333", "16", "4", "44", "3", "7", "G", "<L4G" ], [ "74", "5225", "309", "4.06578", "12.19736", "12", "5.33333", "16", "4", "51", "4", "2", "D", ">D" ], [ "71", "5533", "308", "4.05263", "12.15789", "12", "5.33333", "16", "4", "48", "3", "11", "B", "<B" ], [ "77", "5608", "75", "0.98684", "2.96052", "3", "21.33333", "64", "16", "54", "4", "5", "F", ">L16F" ], [ "79", "5684", "76", "1", "3", "3", "21.33333", "64", "16", "56", "4", "7", "G", "G" ], [ "77", "5760", "76", "1", "3", "3", "21.33333", "64", "16", "54", "4", "5", "F", "F" ], [ "79", "5836", "76", "1", "3", "3", "21.33333", "64", "16", "56", "4", "7", "G", "G" ], [ "67", "5990", "154", "2.02631", "6.07894", "6", "10.66666", "32", "8", "44", "3", "7", "G", "<L8G" ], [ "77", "6297", "307", "4.03947", "12.11842", "12", "5.33333", "16", "4", "54", "4", "5", "F", ">L4F" ], [ "74", "6452", "155", "2.03947", "6.11842", "6", "10.66666", "32", "8", "51", "4", "2", "D", "L8D" ], [ "76", "6759", "307", "4.03947", "12.11842", "12", "5.33333", "16", "4", "53", "4", "4", "E", "L4E" ], [ "79", "7068", "309", "4.06578", "12.19736", "12", "5.33333", "16", "4", "56", "4", "7", "G", "G" ], [ "71", "7221", "153", "2.01315", "6.03947", "6", "10.66666", "32", "8", "48", "3", "11", "B", "<L8B" ], [ "76", "7376", "155", "2.03947", "6.11842", "6", "10.66666", "32", "8", "53", "4", "4", "E", ">E" ], [ "74", "7683", "307", "4.03947", "12.11842", "12", "5.33333", "16", "4", "51", "4", "2", "D", "L4D" ], [ "71", "7992", "309", "4.06578", "12.19736", "12", "5.33333", "16", "4", "48", "3", "11", "B", "<B" ], [ "77", "8068", "76", "1", "3", "3", "21.33333", "64", "16", "54", "4", "5", "F", ">L16F" ], [ "79", "8144", "76", "1", "3", "3", "21.33333", "64", "16", "56", "4", "7", "G", "G" ], [ "77", "8220", "76", "1", "3", "3", "21.33333", "64", "16", "54", "4", "5", "F", "F" ], [ "79", "8296", "76", "1", "3", "3", "21.33333", "64", "16", "56", "4", "7", "G", "G" ], [ "67", "8449", "153", "2.01315", "6.03947", "6", "10.66666", "32", "8", "44", "3", "7", "G", "<L8G" ], [ "77", "8756", "307", "4.03947", "12.11842", "12", "5.33333", "16", "4", "54", "4", "5", "F", ">L4F" ], [ "74", "8908", "152", "2", "6", "6", "10.66666", "32", "8", "51", "4", "2", "D", "L8D" ], [ "76", "9217", "309", "4.06578", "12.19736", "12", "5.33333", "16", "4", "53", "4", "4", "E", "L4E" ], [ "79", "9527", "310", "4.07894", "12.23684", "12", "5.33333", "16", "4", "56", "4", "7", "G", "G" ], [ "71", "9679", "152", "2", "6", "6", "10.66666", "32", "8", "48", "3", "11", "B", "<L8B" ], [ "76", "9832", "153", "2.01315", "6.03947", "6", "10.66666", "32", "8", "53", "4", "4", "E", ">E" ], [ "72", "9934", "102", "1.34210", "4.02631", "4", "16", "48", "12", "49", "4", "0", "C", "L12C" ], [ "77", "10039", "105", "1.38157", "4.14473", "4", "16", "48", "12", "54", "4", "5", "F", "F" ], [ "76", "10142", "103", "1.35526", "4.06578", "4", "16", "48", "12", "53", "4", "4", "E", "E" ], [ "74", "10217", "75", "0.98684", "2.96052", "3", "21.33333", "64", "16", "51", "4", "2", "D", "L16D" ], [ "74", "10293", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "74", "10369", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "74", "10445", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "74", "10521", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "67", "10597", "76", "1", "3", "3", "21.33333", "64", "16", "44", "3", "7", "G", "<G" ], [ "74", "10673", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", ">D" ], [ "67", "10749", "76", "1", "3", "3", "21.33333", "64", "16", "44", "3", "7", "G", "<G" ], [ "72", "11057", "308", "4.05263", "12.15789", "12", "5.33333", "16", "4", "49", "4", "0", "C", ">L4C" ], [ "67", "11210", "153", "2.01315", "6.03947", "6", "10.66666", "32", "8", "44", "3", "7", "G", "<L8G" ], [ "76", "11364", "154", "2.02631", "6.07894", "6", "10.66666", "32", "8", "53", "4", "4", "E", ">E" ], [ "76", "11673", "309", "4.06578", "12.19736", "12", "5.33333", "16", "4", "53", "4", "4", "E", "L4E" ], [ "69", "11775", "102", "1.34210", "4.02631", "4", "16", "48", "12", "46", "3", "9", "A", "<L12A" ], [ "69", "11878", "103", "1.35526", "4.06578", "4", "16", "48", "12", "46", "3", "9", "A", "A" ], [ "77", "11980", "102", "1.34210", "4.02631", "4", "16", "48", "12", "54", "4", "5", "F", ">F" ], [ "67", "12289", "309", "4.06578", "12.19736", "12", "5.33333", "16", "4", "44", "3", "7", "G", "<L4G" ], [ "72", "12392", "103", "1.35526", "4.06578", "4", "16", "48", "12", "49", "4", "0", "C", ">L12C" ], [ "77", "12494", "102", "1.34210", "4.02631", "4", "16", "48", "12", "54", "4", "5", "F", "F" ], [ "76", "12597", "103", "1.35526", "4.06578", "4", "16", "48", "12", "53", "4", "4", "E", "E" ], [ "74", "12675", "78", "1.02631", "3.07894", "3", "21.33333", "64", "16", "51", "4", "2", "D", "L16D" ], [ "74", "12751", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "74", "12827", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "74", "12903", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "74", "12979", "76", "1", "3", "3", "21.33333", "64", "16", "51", "4", "2", "D", "D" ], [ "67", "13063", "84", "1.10526", "3.31578", "3", "21.33333", "64", "16", "44", "3", "7", "G", "<G" ], [ "74", "13130", "67", "0.88157", "2.64473", "3", "21.33333", "64", "16", "51", "4", "2", "D", ">D" ], [ "67", "13206", "76", "1", "3", "3", "21.33333", "64", "16", "44", "3", "7", "G", "<G" ], [ "72", "13513", "307", "4.03947", "12.11842", "12", "5.33333", "16", "4", "49", "4", "0", "C", ">L4C" ], [ "67", "13667", "154", "2.02631", "6.07894", "6", "10.66666", "32", "8", "44", "3", "7", "G", "<L8G" ], [ "76", "13821", "154", "2.02631", "6.07894", "6", "10.66666", "32", "8", "53", "4", "4", "E", ">E" ], [ "76", "14130", "309", "4.06578", "12.19736", "12", "5.33333", "16", "4", "53", "4", "4", "E", "L4E" ], [ "69", "14233", "103", "1.35526", "4.06578", "4", "16", "48", "12", "46", "3", "9", "A", "<L12A" ], [ "69", "14335", "102", "1.34210", "4.02631", "4", "16", "48", "12", "46", "3", "9", "A", "A" ], [ "77", "14437", "102", "1.34210", "4.02631", "4", "16", "48", "12", "54", "4", "5", "F", ">F" ], [ "67", "14747", "310", "4.07894", "12.23684", "12", "5.33333", "16", "4", "44", "3", "7", "G", "<L4G" ], [ "74", "15054", "307", "4.03947", "12.11842", "12", "5.33333", "16", "4", "51", "4", "2", "D", ">D" ], [ "71", "15364", "310", "4.07894", "12.23684", "12", "5.33333", "16", "4", "48", "3", "11", "B", "<B" ], [ "77", "15440", "76", "1", "3", "3", "21.33333", "64", "16", "54", "4", "5", "F", ">L16F" ], [ "79", "15516", "76", "1", "3", "3", "21.33333", "64", "16", "56", "4", "7", "G", "G" ], [ "77", "15591", "75", "0.98684", "2.96052", "3", "21.33333", "64", "16", "54", "4", "5", "F", "F" ], [ "79", "15667", "76", "1", "3", "3", "21.33333", "64", "16", "56", "4", "7", "G", "G" ], [ "67", "15820", "153", "2.01315", "6.03947", "6", "10.66666", "32", "8", "44", "3", "7", "G", "<L8G" ], [ "77", "16128", "308", "4.05263", "12.15789", "12", "5.33333", "16", "4", "54", "4", "5", "F", ">L4F" ], [ "74", "16280", "152", "2", "6", "6", "10.66666", "32", "8", "51", "4", "2", "D", "L8D" ], [ "76", "16589", "309", "4.06578", "12.19736", "12", "5.33333", "16", "4", "53", "4", "4", "E", "L4E" ], [ "79", "16899", "310", "4.07894", "12.23684", "12", "5.33333", "16", "4", "56", "4", "7", "G", "G" ], [ "71", "17049", "150", "1.97368", "5.92105", "6", "10.66666", "32", "8", "48", "3", "11", "B", "<L8B" ], [ "76", "17204", "155", "2.03947", "6.11842", "6", "10.66666", "32", "8", "53", "4", "4", "E", ">E" ], [ "74", "17512", "308", "4.05263", "12.15789", "12", "5.33333", "16", "4", "51", "4", "2", "D", "L4D" ], [ "71", "17820", "308", "4.05263", "12.15789", "12", "5.33333", "16", "4", "48", "3", "11", "B", "<B" ], [ "77", "17896", "76", "1", "3", "3", "21.33333", "64", "16", "54", "4", "5", "F", ">L16F" ], [ "79", "17972", "76", "1", "3", "3", "21.33333", "64", "16", "56", "4", "7", "G", "G" ], [ "77", "18050", "78", "1.02631", "3.07894", "3", "21.33333", "64", "16", "54", "4", "5", "F", "F" ], [ "79", "18126", "76", "1", "3", "3", "21.33333", "64", "16", "56", "4", "7", "G", "G" ], [ "67", "18278", "152", "2", "6", "6", "10.66666", "32", "8", "44", "3", "7", "G", "<L8G" ], [ "77", "18585", "307", "4.03947", "12.11842", "12", "5.33333", "16", "4", "54", "4", "5", "F", ">L4F" ], [ "74", "18739", "154", "2.02631", "6.07894", "6", "10.66666", "32", "8", "51", "4", "2", "D", "L8D" ], [ "76", "19048", "309", "4.06578", "12.19736", "12", "5.33333", "16", "4", "53", "4", "4", "E", "L4E" ], [ "79", "19356", "308", "4.05263", "12.15789", "12", "5.33333", "16", "4", "56", "4", "7", "G", "G" ], [ "71", "19509", "153", "2.01315", "6.03947", "6", "10.66666", "32", "8", "48", "3", "11", "B", "<L8B" ], [ "76", "", "", "", "", "", "faked >", "32", "8", "53", "4", "4", "E", ">E" ] ]

Yikes! What does this all mean?

AThe MIDI note number. (Maybe ought to be between columns L and M.)
BThe absolute time at which the note ends. Not known for the final note.
CThe duration of the note.
DThe normalized duration, using the “magic number” of 76.
EMultiply some of those “near-thirds” out to help in the eventual rounding.
FRounded column E to the nearest integer.
GThis is the ‘inversion’ (1 / column F); the 64 was meant to make integers.
H64 wasn’t enough – those thirds are back! Multiply them out by three again.
INow I see that 64*3 was too much; divide by 4 to reduce.
(Rather than 64*3/4 in columns GHI, I could’ve done just 48, had I known.)
JThe absolute speaker(4) note number (column A − 23).
LThe absolute speaker(4) octave number, calculated by =floor((Jn-1)/12).
MThe relative note number, C = 0 … B = 11.
NThe note name, done via =vlookup to a separate table.
OAt last, the final output for that particular note, combining:
  • The octave specifier:
    • Absolute (eg O4), if the octave had not yet been set; or
    • Absolute, if the target octave differs by more than 1; or
    • Relative (< for −1, > for +1), if the target octave is adjacent; or
    • omitted if the preceding note is within the same octave.
  • The note length (eg L12), only if:
    • the default note length had not yet been set; or
    • it differs from the previous note.
  • The note name (eg C).
For example, the first note encoded is: O4L12C

There are other ways to encode the final output, some more naïve, and some more clever than I have done. A more naïve way would be to output the absolute N note number each time; this is the first way I tried (which is why column K is missing; that held my “version 1” output). One improvement would be to only use the L command when there are multiple notes that have the same duration, specifying explicit note durations for those ‘one-offs’.

Almost done! But – what should the tempo be?

Timing is everything

The file gives the MIDI tempo at 500 000, meaning 120 quarter notes per minute.

Find a note in the output that’s a quarter note long, eg the 12th note. The MIDI file says it takes 307 ticks, but quarter notes are 384 ticks. So the new tempo should be: 120 × 384 ÷ 307 = 150.09772 ≈ 150.

(If you didn’t have a quarter note in the original, you could adjust this by scaling 384 whichever way is easiest, eg using 192 for an eighth note; it’s the ratio that matters.)

Final output for speaker(4)

Listen to abiding in all its glory:

T150O4L12CFEL16DDDDD<G>D<G>L4C<L8G>EL4E<L12AA>F<L4G>L12CFEL16DDDDD<G>D<G>L4C<L8G>EL4E<L12AA>F<L4G>D<B>L16FGFG<L8G>L4FL8DL4EG<L8B>EL4D<B>L16FGFG<L8G>L4FL8DL4EG<L8B>EL12CFEL16DDDDD<G>D<G>L4C<L8G>EL4E<L12AA>F<L4G>L12CFEL16DDDDD<G>D<G>L4C<L8G>EL4E<L12AA>F<L4G>D<B>L16FGFG<L8G>L4FL8DL4EG<L8B>EL4D<B>L16FGFG<L8G>L4FL8DL4EG<L8B>E

Other TempleOS hymns

I converted two of my other ‘favourites’ in more or less the same way. Enjoy!

abyss

T50O3L48BBBB>L12C<L8BL24AL12BL4A>L24EC<L12A>L48BBBB>L12C<L8BL24AL12BL4AL24DDL12DDL24EDL12GCL24F<G>L12GL48FGFGL24DCL12DL24EDL12GCL24F<G>L12GL48FGFGL24DC<L48BBBB>L12C<L8BL24AL12BL4A>L24EC<L12A>L48BBBB>L12C<L8BL24AL12BL4AL24DDL12DDL24EDL12GCL24F<G>L12GL48FGFGL24DCL12DL24EDL12GCL24F<G>L12GL48FGFGL24DC

almighty

T50O3L12G>L36C<GGL4AL36G>F<B>L12EL24FE<L12G>L36C<GGL4AL36G>F<B>L12EL24FEL12FFEEL24E<B>L12FC<B>FFEEL24E<B>L12FC<BG>L36C<GGL4AL36G>F<B>L12EL24FE<L12G>L36C<GGL4AL36G>F<B>L12EL24FEL12FFEEL24E<B>L12FC<B>FFEEL24E<B>L12FC<B

Future work

Obviously the most-wanted feature is automation. The reason I didn’t start that way is because I wanted to make sure my quantization was working properly, and it’s much easier to do that visually in ExcelGoogle Sheets. (To be fair, I did the numerical error analysis in Numbers.)

The quantization could use some improvements. I think actually a lot of that is tied to error analysis, to try to find the most optimal value possible.

One idea would be to try to fit the MIDI note durations to the entire gamut of available speaker(4) durations. Using 384 ticks per quarter note, the ‘standard’ durations are:

NoteDurationTicks
[ [ "sixty-fourth note", "1/64", "24" ], [ "dotted sixty-fourth note", "3/128", "36" ], [ "thirty-second note", "1/32", "48" ], [ "dotted thirty-second note", "3/64", "72" ], [ "sixteenth note", "1/16", "96" ], [ "dotted sixteenth note", "3/32", "144" ], [ "eighth note", "1/8", "192" ], [ "dotted eighth note", "3/16", "288" ], [ "quarter note", "1/4", "384" ], [ "dotted quarter note", "3/8", "576" ], [ "half note", "1/2", "768" ], [ "dotted half note", "3/4", "1152" ], [ "whole note", "1/1", "1536" ], [ "dotted whole note", "3/2", "2304" ] ]

However, this still leaves gaps. For instance, how do you represent a half note tied to an eighth note – that is, a 58 note, or 960 ticks? There are no ties in this language, so the only two things left are to use dots, or uncommon note durations.

The FreeBSD manpage mentions that, aside from a single dot, multiple dots in speaker(4) aren’t musically standard lengths:

The action of two or more sustain dots does not reflect standard musical
notation, in which each dot adds half the value of the previous dot modi-
fier, not half the value of the note as modified. Thus, a note dotted
once is held for 3/2 of its undotted value; dotted twice, it is held 7/4,
and three times would give 15/8. The multiply-by-3/2 interpretation,
however, is specified in the IBM BASIC manual and has been retained for
compatibility.

Let’s look at uncommon note durations. Here are all of the various durations between an eighth note and a dotted whole note:

DurationTicks
speaker(4)fractional
[ [ "C8", "1/8", "192" ], [ "C11.", "3/22", "209.45" ], [ "C7", "1/7", "219.43" ], [ "C10.", "3/20", "230.4" ], [ "C6", "1/6", "256" ], [ "C8.", "3/16", "288" ], [ "C5", "1/5", "307.2" ], [ "C7.", "3/14", "329.14" ], [ "C4", "1/4", "384" ], [ "C5.", "3/10", "460.8" ], [ "C3", "1/3", "512" ], [ "C4.", "3/8", "576" ], [ "C2", "1/2", "768" ], [ "C2.", "3/4", "1152" ], [ "C1", "1/1", "1536" ], [ "C1.", "3/2", "2304" ] ]

You could scale a quarter note to be a dotted tenth note (ie, C10. or 230.4 ticks). Then a 58 note would be scaled to a dotted quarter note (ie, C4. or 576 ticks). You can do this with other combinations, eg C15 (102.4 ticks) and C6 (256 ticks). The key is the ratio between the relative durations; in this case, the length of a half note tied to an eighth note is 2.5 times that of a quarter note.

To misquote Buckley’s, it seems awful, and it works.

When you do this sort of scaling, the tempo must be adjusted to compensate. Using a fifteenth note as the ‘new’ quarter note means the tempo should be divided by 3.75 (384 ÷ 102.4). Notice that, for an original tempo of 120, this gives 32, the slowest possible tempo. So the original tempo determines how much scaling one can apply.

It could be that, in order to perform the best overall quantization possible – or to avoid wacky scaling – some notes would have to be played twice, for example C2C8 for a half note tied to an eighth note. Alternately, the note could be shortened as C2P8 – whichever seems more appropriate for the circumstances.

Use the source

Another idea: just convert the existing music notation directly (well, as directly as possible).

I dug up the hymn sources in the Wayback Machine, eg:

Appendix: Downloads

As mentioned in an earlier section, I found the MIDI files to be lacking Note off  events, so I wrote a small script to add them in. Here is the result:

Eventually I would like to finish converting the rest of the hymns and make them available for download here, too.

Appendix: Playing these in BASIC

The day after I wrote this note, I remembered using this same syntax elsewhere, many years ago: in MS-DOS QBasic. Then it occurred to me that I could probably get QBasic running in DOSBox easily enough.

There are already sites that explain how to run QBasic in DOSBox (although technically those are instructions for QuickBASIC ) so I won’t repeat that here. For my own setup, I just extracted QBASIC.EXE from the MS-DOS 6.22 Setup Disk 1, and put it and my tunes in a directory, which I mounted in DOSBox.

To actually play a tune, you just need to put it between double quotes and add the PLAY statement at the beginning. Here’s all three tunes in the same BASIC file:

REM abiding
PLAY "T150O4L12CFEL16DDDDD<G>D<G>L4C<L8G>EL4E<L12AA>F<L4G>L12CFEL16DDDDD<G>D<G>L4C<L8G>EL4E<L12AA>F<L4G>D<B>L16FGFG<L8G>L4FL8DL4EG<L8B>EL4D<B>L16FGFG<L8G>L4FL8DL4EG<L8B>EL12CFEL16DDDDD<G>D<G>L4C<L8G>EL4E<L12AA>F<L4G>L12CFEL16DDDDD<G>D<G>L4C<L8G>EL4E<L12AA>F<L4G>D<B>L16FGFG<L8G>L4FL8DL4EG<L8B>EL4D<B>L16FGFG<L8G>L4FL8DL4EG<L8B>E"

REM abyss
PLAY "T50O3L48BBBB>L12C<L8BL24AL12BL4A>L24EC<L12A>L48BBBB>L12C<L8BL24AL12BL4AL24DDL12DDL24EDL12GCL24F<G>L12GL48FGFGL24DCL12DL24EDL12GCL24F<G>L12GL48FGFGL24DC<L48BBBB>L12C<L8BL24AL12BL4A>L24EC<L12A>L48BBBB>L12C<L8BL24AL12BL4AL24DDL12DDL24EDL12GCL24F<G>L12GL48FGFGL24DCL12DL24EDL12GCL24F<G>L12GL48FGFGL24DC"

REM almighty
PLAY "T50O3L12G>L36C<GGL4AL36G>F<B>L12EL24FE<L12G>L36C<GGL4AL36G>F<B>L12EL24FEL12FFEEL24E<B>L12FC<B>FFEEL24E<B>L12FC<BG>L36C<GGL4AL36G>F<B>L12EL24FE<L12G>L36C<GGL4AL36G>F<B>L12EL24FEL12FFEEL24E<B>L12FC<B>FFEEL24E<B>L12FC<B"

Each PLAY statement should be all on one line – they’re wrapped here only for readability.

Here’s what it looks like in DOSBox on macOS:

TAD Hymns in QBasic running on DOSBox on macOS

Finally, I made a video showing how it sounds in QBasic. The output was changed slightly so that it was clear in the video which tune was currently being played.

The output is a bit more wobbly than on a real PC speaker, and has some ‘interesting’ harmonics, but otherwise seems accurate.


Feel free to contact me with any questions, comments, or feedback.