← back to posts
                   ,»»»»»»»»»»≡»≡≡»»«≡≡≡╔╔╔╔╔╔╔╔╔╔╔╔╔,
                   !░│¡░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒
                   !░!)φ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╠╠
                   ]░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓]
                   ]░░╬╬╬╬╬╬╬╬╬╬╬╬╬╬▓▓]
                   ]░░╬╬╬╬╬╬╬╬╬╬╬╣╣▓▓▓▓▒╡
                   ]░░╬╬╬╬╬╬╬╬╬╬╬╬╣▓▓▓▓▓▓▒╡
                   ]╬╬╬╬╬╣╬╣╣╣╣╣▓▓▓▓▓▓╠╡
                   [▓╣╣╣╣╣▓╣▓▓▓▓▓▓▓▓▓▓██╠╡
                   [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██╠╡
                   ]▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████╠╡
                   [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█████╠╡
                   [░░└└└└└╙╙╙╙╙╙╙╙╙╙╙╙╙╙╙╙╙╙╙ ╠╡
                   [░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╡
                   [░░░░░░░░░░░░░░░░░░░░░░▒▒▒░▒▒▒▒▒▒╡
                   [░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒╠╡
                   Γ░ΓΓΓΓ░╚╚░╚╚╚╚╚╚▀▀▀▀▀▀▀▀▀▀▀▀▀╠╠Γ
                   [░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒╠╠╡
                   φ░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒╠╠
                   ░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒╠Γ
                   ]╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬
              _____]╣╣▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓____
       _ƒⁿ╙]M░ƒ∩░%∩░╔╔∩⌐ƒMƒƒM5²MδƒMΘφM░j∩>%^^(φƒ╕░ƒƒp░░Q
       .7DDφ≥▒5≥▒φ≥▒δUΓφ7ΓêÜê5Üφ5Üδ≤Üê5▒φ╠▒φ⌂Då5_"S"S"""Å"ⁿ-.,
╚╚╚δ╚╚δδδ╚╚╚╚╚╚╚╚╚╚╚╚╚╚╚╚╚╚╚╚╚W╚╚⌐╚╚W╚╚⌐╚╚Ü.-W╚╚╚╚╚ë╚╚ë╚╚Θ░      __`-
     =Q░░░░░░░░░░░░░░░░░░░░░░▒▒░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒

Hours

My latest project is live at horarum.com.

Hours is my app for kids who can't read neumes good and who wanna learn to do other stuff good too, like pray the Roman Breviary.

I've made a number of attempts over the years to learn both the Roman Breviary and Gregorian notation. I had some success with each but never quite got the hang of either. I was never 100% sure I had handled the edge cases correctly, and by the time I looked up the rubrics and found the right setting for the chant or office, I was often discouraged from doing it at all.

Over the years I collected various aids meant to make this easier. Eventually I found Breviarium Gregorianum, which is basically the perfect version of the idea: the correct office, Latin and English, and the chant all together in one place.

It's an excellent website and Hours owes a lot to it. My only real issues were that Matins and some of the larger offices could take a while to load, a few days were incomplete, and I still wanted the speed, offline use, and general feel of a native iPhone and iPad app.

The Hours home screen using the sundial display.The Hours home screen using the canonical hour wheel.

The sundial and wheel displays.

Building the Office Before Opening It

My first thought was to bundle the text, scores, calendar, and rubrics and let the app assemble each office as needed. That's more or less what I ended up doing, except it proved much simpler and more efficient to make all the calendar and rubrical decisions ahead of time. The build pipeline applies the rubrics, validates everything the app will need for the next 10 years, and stores the result in a 74 MB database. That comes to 35,064 scheduled offices. By the time the database reaches the app, all of the difficult decisions have already been made. The app just looks up the office for a particular date and hour and loads its sections.

The database ended up with a pretty simple shape:

+------------------------+     +------------------------+
| office_schedule        |     | recipes                |
+------------------------+     +------------------------+
| FK recipe_id           | N:1>| PK id                  |
| PK date                |     | UQ stable_key          |
| PK hour                |     | payload: office header |
+------------------------+     +-----------+------------+
                                         v 1:N
+------------------------+     +-----------+------------+
| text_resources         |     | recipe_sections        |
+------------------------+     +------------------------+
| PK id                  |<N:1-| FK text_id             |
| UQ stable_key          |     | FK score_id (nullable) |
| kind, incipits_latin   |     | PK/FK recipe_id        |
| payload: text / rubrics|     | PK position            |
+-----------+------------+     +-----------+------------+
            v 1:1 by rowid                 v optional N:1
+-----------+------------+     +-----------+------------+
| text_fts               |     | scores                 |
+------------------------+     +------------------------+
| title / rubric         |     | PK id                  |
| Latin / English        |     | UQ stable_key          |
| chant incipits         |     | payload: GABC/timeline |
+------------------------+     +------------------------+

A schedule maps each date and canonical hour to an office recipe, and each recipe is just an ordered list of headings, rubrics, psalms, readings, prayers, and chants. The actual text and chant scores live separately so they can be reused anywhere they appear. The search index points back to every office where a shared text is used.

Keeping those parts separate makes the size manageable. The build pipeline can also validate each reusable piece once, check its order in every recipe, and make sure each date and hour points to something complete.

Getting Rid of the WebView

The first version used Exsurge inside a WebView. That proved the basic idea quickly, but putting the text and notation in a little web island inside SwiftUI defeated a lot of the reason I wanted a native app. So I ported the parts I needed to Swift.

The Swift version starts with GABC and turns it into a structured score with lyrics, syllables, clefs, notes, neume forms, accidentals, divisions, and the other marks needed to engrave chant. A separate layout engine works out the line breaks, positions the neumes and staff, centers lyrics around their vowels, adds the clef and custos for each line, and produces the paths SwiftUI draws with Canvas. The glyph outlines still come from Exsurge, but none of its JavaScript or WebView rendering ships in the app.

Each sung note keeps the same identity from the compiled GABC through layout and playback. That lets me tap a neume to start there, see the current note highlighted, and keep the active line on screen while the Cantor Guide plays.

Hours displaying the complete chanted office of Lauds on an iPad.

A complete office laid out and rendered natively on iPad.

Using Hours

Most of the time, I open the app, tap Pray, and the office for the current date and canonical hour appears. If I don't know one of the chant settings, I can tap that section to listen to it. This is especially helpful for the less common chants and hymn settings that show up on feast days, during Holy Week, and so on.

There is also a calendar explorer and search for finding a particular office, hymn, prayer, or reading.

Search covers office titles, readings, hymns, psalms, antiphons, responsories, collects, and anything with a chant setting. Results can be narrowed to Latin or English and to a particular canonical hour. Searching for a feast shows the dates and hours where that office occurs; searching for a prayer or reading shows the text, its available chant settings, and the offices where it is used. From there the app can open the full office on the relevant date or open the chant directly in the Cantor Guide.

The Hours search screen showing recently searched terms and recently viewed prayers and readings.
Title search results for Saint Louis, grouped by canonical hour and date.
Search results for Salve Regina across chants and office sections.

Recent searches and results for an office title and a chant text.

Search runs against SQLite's full-text index in the bundled database. The build pipeline indexes titles, rubrics, chant incipits, and the Latin and English text as it builds the corpus. It also smooths over a few editorial differences in the Latin, including accents, æ versus ae, œ versus oe, and j versus i. That helps when the same prayer has passed through books and transcriptions from different periods that don't all make the same typographic choices.

The Salve Regina result detail screen showing two chant settings and the offices where the antiphon is used.
The Salve Regina chant score opened in the Hours Cantor Guide.

Tapping a chant result shows its settings and uses; selecting a setting opens the score in the Cantor Guide.

Sources and Inspiration

Hours depends on a lot of work other people have made freely available. The material used in the app comes from:

  • Divinum Officium for the 1960 calendar logic, Latin office text, and English translations.
  • GregoBase for public-domain chant transcriptions.
  • Nocturnale Romanum for additional Matins chant material.
  • Exsurge for the Gregorian glyph outlines that I convert into native Swift paths.
  • EB Garamond for the app's text and chant lyrics.

The projects I leaned on most heavily as references and inspiration were:

  • Breviarium Gregorianum for the basic idea, the reading experience, and a reference for how complete offices and chants fit together.
  • The original Exsurge project, which remained my visual reference while I replaced the WebView renderer with native Swift.
  • jgabc / Chant Tools for generating and checking the formulaic psalm, lesson, and chapter tones during the content build.