Introduction

John Coltrane

Trane is an automated practice system for the acquisition of arbitrary, complex, and highly hierarchical skills. That's quite a mouthful, so let's break it down.

  • Practice system: Deliberate practice is at the heart of the acquisition of new skills. Trane calls itself a practice system because it is designed to guide student's progress through arbitrary skills. Trane shows the student an exercise they can practice and then asks them to score it based on their mastery of the skill tested by the exercise.
  • Automated: Knowing what to practice, when to reinforce what has already been practiced, and when to move on to the next step is as important as establishing a consistent practice. Trane's main feature is to automate this process by providing students with an infinite stream of exercises. Internally, Trane uses the student feedback to determine which exercises are most appropriate for the current moment.
  • Arbitrary: Although originally envisioned for practicing Jazz improvisation, Trane is not limited to a specific domain. Trane primarily works via plain-text files that are easily sharable and extendable. This allows student to create their own materials, to use materials created by others, and to seamlessly combine them.
  • Complex and hierarchical skills: Consider the job of a master improviser, such as the namesake of this software, John Coltrane. Through years of practice, Coltrane developed mastery over a large set of interconnected skills. A few examples include the breathing control to play the fiery stream of notes that characterize his style, the aural training to recognize and play in any key, and the fine motor skills to play the intricate lines of his solos. All these skills came together to create his unique and spiritually powerful sound. Trane is designed to allow students to easily express these complex relationships and to take advantage of them to guide the student's practice. This is the feature that is at the core of Trane and the main difference between it and similar software, such as Anki, which already make use of some of the same learning principles.

Trane is based on multiple proven principles of skill acquisition, like spaced repetition, mastery learning, interleaving, and chunking. For example, Trane makes sure that not too many very easy or hard exercises are shown to a student to avoid both extremes of frustration and boredom. Trane makes sure to periodically reinforce skills that have already been practiced and to include new skills automatically when the skills that they depend on have been sufficiently mastered.

If you are familiar with the experience of traversing the skill tree of a video game by grinding and becoming better at the game, Trane aims to provide a way to help students complete a similar process, but applied to arbitrary skills, specified in plain-text files that are easy to share and augment.

Trane is named after John Coltrane, whose nickname Trane was often used in wordplay with the word train (as in the vehicle) to describe the overwhelming power of his playing. It is used here as a play on its homophone (as in "trane a new skill").

The Trane Project's Guide to Learning Music by Ear

Introduction

This document describes a process of transcribing music with the purpose of learning it by ear. This process is not my creation, but rather a distillation of the ideas of many musicians who have come to their own conclusion about how music is learned by ear. The result is a process that it is effective and fun.

This process is very similar to what is commonly referred as transcription. Transcription is well-known to lead to great results, but it is thought of as difficult and frustrating. The process described here is a modified version of transcription intended to reduce the difficulty of the common methods.

Transcription

First, let's introduce the specific meaning of the term transcription that will be use from now on. Normally, transcription refers to the process of either writing down a piece of music or playing it on an instrument as it was played in a recording.

The instructions are usually as follows:

  1. Listen to a piece of music several times until you can sing or hum it comfortably.
  2. Try to play it back on your instrument.
  3. Optionally, write it down in musical notation.

The most common problems that students find when trying this method are:

  1. Beginners often find it very hard to find the exact notes that were played.
  2. One is often told to listen to the recording, stop it, and then play the notes back. This relies on the student's musical memory, which is not very reliable at the beginning, adding to the frustration.

For our use case, we'll need a wider definition of the term. The process described above is included in this expanded definition, but it's only the last part of it. Furthermore, writing down the music is not required at all, although students are free to do so.

The problem with the first definition is that the process is too focused on the exact notes that were played. There is a lot more to the music than that. Each piece creates a musical context that the perspective student must first learn to navigate. Our process begins by playing within this context without many regards for the intricacies of what was actually played. It is only after this context has been sufficiently approximated by the student that the focus can narrow down to the notes that were played. However, the focus is always kept slightly open. The end goal is not perfectly accurate reproduction of the music, but its full internalization into the personal style of the student.

Materials

The materials needed for this process are:

  1. Your instrument.
  2. The recording of a song you want to learn.
  3. A music player. The recommended music player is Transcribe!. This is a specialized music player for transcription that allows for looping sections of a song, slowing it down, and shifting the pitch up and down. It is paid software, but it is well worth the price.

Process

There are four main phases in the process, each of which builds on the previous ones:

  1. Singing
  2. Transcription
    • Depends on sufficiently mastering the singing phase.
  3. Advanced Singing
    • Depends on sufficiently mastering the singing phase.
  4. Advanced Transcription
    • Depends on sufficiently mastering the advanced singing lesson and the transcription lesson.

For all of these phases, you first practice them with the music playing in the background, and then without the music once you are comfortable. Doing things in this order removes the initial frustration of having to remember the passage and keeps the music in your ear without much effort. You can focus on different elements or sections each time you practice each phase.

Singing

First listen to the musical passage until you can audiate it clearly in your head. Then sing over the passage. At this stage it's not required to be accurate as possible. Rather, learn to sing the main elements of the passage and experiment with different melodies over it. The goal is to learn to navigate the context implied by the passage using your voice. You can use your instrument to keep you in check.

Transcription

With the basic context implied by the passage now internalized in your ear, try to play over it using your instrument. The goal at this point is not to accurately reproduce the passage, but rather about learning to navigate that context and use it as a basis for improvisation. Feel free to experiment, and find out for yourself what works and what doesn't.

Advanced Singing

At this stage, you should sing the passage with more detail and precision using solfège. You also pitch the music up and down a random number of semitones to practice in a variety of keys. However, while the focus is narrower, you should still experiment and play with the music.

The recommended solfège system is movable do, with a la-based minor. In this system, the note for the root of the key is always represented by do, and the modes are represented by the different syllables. The minor mode, for example, starts with the syllable la. This system has the advantage that the same syllables can be used for all modes. For the syllables used in the recommended solfège system, see this link.

Advanced Transcription

At this stage, you play over the passage, and sing it with more detail and precision. It's at this point that you can engage in what is traditionally called transcription. You should also transpose the passage up or down a random number of semitones. The passage is still used as a basis for improvisation, but the focus is much narrower than in the basic transcription lesson, and more attention is paid to details such as articulation, phrasing, etc.

Transcription Tips

In regard to what notes to play, the best way to start is to play any note. Most music you will want to learn is based on a variation of the Major scale. If you play any note, you either landed on a note in the key or the two notes surrounding your guess are in the key.

This tip, simple as it might appear, is very profound. In practical terms, this tip means that you can navigate almost any melodic and harmonic situation without fear of getting lost. If you do get lost, all you have to do to regain your footing is to once again play any note, hear if you are on or off-key, and act accordingly. You are never more than a half step away from a note in the key.

Initially you have a perception of a note being on or off-key. As your ear develops, you will start to notice the degree of the note.

More Resources

Here are some resources that I've found useful when figuring this out.

  • Improvise For Real: A method for learning to improvise by ear. It's the first place where I was told that trick about playing any note. Practicing this exercise over a long weekend unlocked me mentally and allowed me to jam and have fun with music. As such, it gets the first mention.
  • Hal Galper - The Illusion of an Instrument: A mind-blowing master class by Hal Galper. The core of this process is contained there, although I was not ready to understand it when I first saw it.
  • The Music Lesson: This book by Victor Wooten contains a lot of the same ideas, and focuses on a lot of elements that are normally not touched on in other books. Also found the tip on playing any note here, although after finding it in the IFR method.
  • Interview with Hal Galper: A full interview with Hal Galper. It's an hour long but worth it. Goes through similar material as the master class, but in full detail.
  • Interview with Mike Longo: An interview with Dizzy Gallers' pianist, Mike Longo. He's mentioned in the previous interview. Here he talks about the apprenticeship model. Full of gems.

Slides

Below is a deck of slides that contain the same ideas as this page, including more historical background and how to use Trane to help you with this process.

Video

Below is a video of the slides being presented with some additional commentary and musical examples.

Quick Start

For technical users

This section assumes you know the terminal for your operating system, and can install software in a place where your terminal can find it.

Install trane-cli

Prebuilt binaries

trane-cli is the command-line interface for Trane. To install it download the appropriate binary for your operating system and CPU architecture from the Releases page. Then move the binary to a directory in your PATH environment variable or somewhere else where your terminal can find it.

From source

If you want to build the binary from source, you need to have the Rust toolchain installed. Then, clone the trane-cli repository and run the command cargo install --path .

Create your first Trane library

A Trane library is simply a directory with a .trane directory in it. Your progress and options are stored under this directory. This directory is created and populated automatically the first time you open it with Trane. Thus, the only thing needed to create a new Trane library is to create a new directory.

Opening your Trane library

From the directory you just created, open a terminal, and run the command trane. You should see a prompt like this:

trane >> 

Inside this command-line interface, type open . followed by the Enter key. You should see a message saying that the library is open. To see a new exercise, type next. Since there are no materials downloaded yet, you'll get a message saying there was an error retrieving an exercise.

If you have difficulties navigating the command-line interface, refer to the "Basic shortcuts" section in the trane-cli documentation.

Downloading material for Trane.

For this guide, we'll use the official Trane course trane-music. Go to that webpage and click on the green button that says "Code". In the "Local" tab, there are options for cloning the repository. Click on the option that says "HTTPS" and copy the URL shown. In this example, it is https://github.com/trane-project/trane-music.git.

In the terminal, type the following command:

repository add https://github.com/trane-project/trane-music.git

This will download the contents of that repository into your computer in a directory called managed_courses. You can verify that the repository was downloaded by executing the command repository list.

Note: If you are using git to sync your Trane library, you should add the managed_courses directory to your .gitignore file. If you do not understand what the previous sentence means, you do not need to worry about it.

Working on your first exercise

Without exiting Trane, refresh the library by executing the command open .. Now that there are exercises in the library, you can execute the command next to get a new exercise. You should see the exercise on your screen. If an answer is available, you can see it by executing the command answer. Providing a score for the exercise is done by executing the command score <YOUR_SCORE>, where <YOUR_SCORE> is a number between 1 and 5 that indicates your mastery of the exercise. Once you have provided a score, you can get a new exercise by executing the command next.

For non-technical users

For less technical users, the only difference with respect to the previous section is that you should download the prebuilt trane binary and place it in the directory you created to store your Trane library. This way, you don't need to install it in a directory in your PATH environment variable if you don't know what that means. All the other steps are the same.

Concepts and Theory

Concepts

This section defines basic concepts used in Trane, useful both for using it and for creating new material.

Skills

In Trane, a skill refers to a specific ability to perform a task that can be improved through the practice of exercises. The result of said practice should result in the reduction or elimination of the cognitive load needed to perform the task and the error rate. This definition covers a wide range of tasks, including memorization. For example, learning the multiplication tables is a skill when considering the need to retrieve an answer quickly and without error when performing mental calculations. Skills can often be divided into smaller tasks that can be practiced separately. For example, learning a new piece of music can be broken down into learning individual sections of the piece.

Mastery Score

When presented an exercise, a user performs it and assigns it a score signifying their mastery of the task. The scores range from one to five, with one meaning the skill is just being introduced (e.g., reading a section of a music score and figuring out the notes and movements required to play it) and five meaning complete mastery of the material (e.g., effortlessly playing the section and improvising on it).

Dependency Graph

Just as skills can be broken down into multiple smaller skills, they can also be built up in order to perform more complicated tasks. This process requires that the dependency relationships between skills are described. For two given skills S1 and S2, S1 is a dependency of S2 if S2 cannot be learned properly until S1 is sufficiently mastered. For the reverse relationship, S2 is called a dependent skill of S1.

The set of all these relationships between skills forms a directed acyclic graph and is called the dependency graph. Trane uses this graph to make sense of a student's progress and determine which skills should be introduced next and which previously practiced skills should be reviewed.

Units

There are three types of units in Trane:

  • Exercise: An exercise is just a task which needs to be performed and assigned a score. Exercises do not define any dependencies.
  • Lesson: A set of related exercises, which ideally follow the same format and are related to the same skill. Lessons can depend on other lessons and courses.
  • Course: A set of lessons on a related topic which test and build up related skills. Courses can depend on other courses and lessons.

Units are defined in plain text files that are read by Trane during startup. The format of these files is described in the section on Writing Trane Courses.

Library

A Trane library is a set of courses stored under the same directory. Courses can be stored under any directory structure, but the content of each course follows a predetermined structure (See the section on Writing Trane Courses). Trane stores its configuration under a directory called .trane in that directory. Users might want to have multiple separate libraries if they are learning separate skills (e.g., music and chess), and they want to keep their practice separate.

Blacklist

Each Trane library has a blacklist. A unit in a blacklist can be any exercise, lesson, or course. If a unit is in the blacklist, Trane will not show any exercises from it. If a lesson or course depend on a blacklisted unit, the scheduling algorithm will act as if the blacklisted unit has been mastered.

A unit should be added to the blacklist if the user already has mastered the material (e.g., an accomplished guitarist will want to skip the course teaching how to tune the guitar). It can also be added if the user has no interest in learning the material (e.g., someone interested in learning the guitar might want to skip units which are focused on another instrument).

Filters

In its normal mode of operation, Trane looks for exercises in the entire library. There are times when users might want to focus on a smaller section. Filters provide users with the ability to select specific exercises. There are three types of filters.

  • Lesson filter: Only present exercises from the given lesson. For example, users might want to only practice exercises from a lesson covering a section of a song.
  • Course filter: Only present exercises from the given course. The dependency relationships among the lessons in the course are respected. For example, users might want to only practice exercises from a course covering an entire song.
  • Metadata filter: Courses and lessons can have key-value pairs as metadata. A metadata filter acts on this metadata to present exercises exclusively from units which match the filter, while also preserving the dependency relationships between those lessons. Lessons which do not pass the filter are considered as mastered so that the scheduler can continue the search. For example, a user might want to only practice exercises from lessons and courses for the guitar and in a specific key.
  • Dependent filter: Given a set of courses or lessons, Trane will search for exercises in those and all the units that are dependent on them. For example, a user might want to practice exercises from all the lessons with intermediate or higher difficulty while skipping easier lessons.
  • Dependency filter: Given a set of courses or lessons and a depth, Trane will search for all the units that are dependent on the given units up to the given depth, and start the exercise search from there. For example, a student that encounters some difficulties in a course, might want to practice exercises from the course and all the units that immediately precede it to refresh their memory.

Review List

The review list stores units which the student feels should be given special attention. Trane includes a special filtering mode which only presents exercises from the review list. If a course or lesson is added to the review list, all of its exercises can appear in this mode.

Theory

This section describes the ideas behind Trane and its design.

Mastery Learning

Mastery learning states that students must achieve a level of mastery in a skill before moving on to learning the skills which depend on the current skill. Conversely, students must sufficiently master all the dependencies of a skill before they can learn it.

Trane applies mastery learning by preventing the user from moving on to the dependents of a unit until the material in the unit is sufficiently mastered. It also excludes units whose dependencies have not been fully met. Otherwise, a user might be presented with material that lies outside their current abilities and become frustrated. If a user's performance on a previously mastered unit degrades, Trane will ensure that the unit is mastered again before showing any of its dependents.

Spaced Repetition

Spaced repetition is a long-established way to efficiently memorize new information and to transfer that information to long-term memory. When a concept or skill is first introduced, it is presented more frequently. Progressively, as the student gains mastery of it, the frequency is reduced, although mastered exercises are still shown from time to time for review and maintenance.

Trane applies spaced repetition to exercises that require memorization (e.g., recalling the notes in the chord A7) and to those which require mastery of an action (e.g., playing a section of a song). The space repetition algorithm in Trane is fairly simple and relies on computing a score for a given exercise based on previous trials rather than computing the optimal time at which the exercise needs to be presented again.

Chunking

Chunking consists of breaking up a complex skill into smaller components that can be practiced independently, before the individual components are combined in more complex skills.

Trane applies chunking by allowing users to define lessons and courses with arbitrary dependency relationships. For example, learning to improvise over chord progressions might be broken into units to learn the notes in each chord, learn the fingerings of each chord, or improvise over single chords. The user can then define a unit that exercises the union of all the previous skills and claim the other lessons as a dependency.

Interleaving

Interleaving is the practice of mixing up the order in which skills are presented, instead of showing consecutive exercises that test the same or similar skills. This strategy has been shown to lead to improved retention and recall of the material.

Trane applies interleaving as follows. In the first phase of computing a batch of exercises to study, Trane randomly selects branches of the dependency graph when searching for exercises that the student is allowed to practice. In the second phase, which consists of reducing the exercises found in the first phase into the final batch, Trane randomly selects which exercises to include in the batch. The probability of an exercise being included in the batch is weighted based on a number of factors, such as the exercise's score, the depth of the exercise within the dependency graph, and the number of times it has been shown in the same session.

Mixed Difficulties

Similar to interleaving, mixed difficulties consists of mixing exercises of different difficulty when presenting them to the student. This strategy is meant to avoid showing too many easy exercises, which would bore the student and slow progress on more relevant exercises, or too many hard exercises, which would frustrate the student and lead to a loss of motivation.

Trane applies mixed difficulties by selecting exercises from a range of difficulties. In the second phase outlined in the previous section, Trane groups exercises into buckets based on some ranges difficulties, and selects a fixed percentage from each bucket. A small percentage of the exercises will be selected from the easiest and hardest buckets, while the majority will be from one of the buckets in the middle.

Vision

Trane aims to offer a coherent and powerful user experience that is capable of taking students of any level and allowing them to make the most out of their practice sessions, as well as offering features to allow teachers to track and guide the progress of their students. Trane will attempt to fulfil those goals by making progress along main four axes. Those are described below using musical training as an example, but any skill for which the proper exercises and the dependencies among them can be derived should be an apt candidate for Trane.

Defining arbitrary skill graphs

Complex skills are in fact a complex network of simpler skills and domain knowledge that have been acquired after many years of practice. Experts on a field might not be consciously aware of the full network, either because some of it is the result of unconscious processes or because it was learned in early childhood or in an unstructured manner. Trane's first goal is to allow this graph to be defined and reused for the purpose of teaching other people.

To succeed at this task, Trane must be able to handle a large set of exercises as well as their dependencies in a way that allows users to define arbitrary relationships between them, to seamlessly combine exercises from multiple sources, and to provide primitives to select particular sections of the graph to focus on or to ignore during practice sessions.

As an example, the skill of playing a large musical piece depends on the skills of playing each movement, which in turn depend on skills to play each passage. By defining the practice of the piece on this manner, the student is first asked to master individual passages, then individual movements, and eventually the entire piece.

Automated and guided progress

Having defined an extensive skill graph, students should be able to take advantage of it without lots of effort or planning. In its default mode, Trane provides exercises to the user that gradually introduce students to new skills and reinforce the ones already in the process of being mastered. Trane also takes care of keeping the difficulty of the exercise balanced so only a small percentage of very hard and very easy questions are shown. In doing so, Trane ensures that the student is slightly challenged without falling into any extreme of frustration or boredom.

Trane's main mechanism for guiding progress is to allow students to rate the mastery of each trial of an exercise. Trane uses the past scores to decide which exercises to show, when to stop traversing the dependency graph, how often to show an exercise, and how to balance the difficulty of each batch of exercises.

In combination with its abilities to handle large graphs of dependency relationships, Trane also allows students to focus on specific courses, lessons, or on material that matches specific metadata, while still keeping the guarantees mentioned above. For example, a student could choose to focus on exercises made for the guitar while Trane still makes sure that harder guitar exercises are not shown to the student until easier guitar exercises are sufficiently mastered.

Plain text configuration files for easy sharing and collaboration

The task of defining the dependency graphs for a skill is impossible for a beginner to undertake because they lack the experience needed to understand the full picture. It is also difficult for teachers because the task is big, and they might lack the technical knowledge to do it. However, one of Trane's assumptions is that the majority of learners will follow the same path to mastering a skill, so the task of defining the graph needs to be undertaken once.

To make collaboration and sharing easier, Trane provides a simple text file format for defining the exercises, lessons, and courses and their relationships. This allows these files to be automatically generated and managed in version control software. Trane also provides utilities that make it easier to generate these files and to verify the generated output.

By allowing the generation of these files, one could define a course that asks a student to perform some exercises in each key without the burden of manually defining the files for each key. And by allowing these files to version controlled, it is our hope that students can easily get started by taking advantage of official courses and those created by the community.

Integration with teachers materials and feedback

Note: Most of the ideas in this section are not yet implemented.

Another of Trane's goals is to integrate teachers into the process. The first way Trane achieves this is by letting them integrate their own materials with existing materials. For example, a teacher could add additional exercises to a lesson, or add new courses that depend on existing ones.

Another way is to let teachers provide feedback on the progress of students. While these ideas are not yet implemented, the goal is to allow teachers to rate the mastery of their students in a given exercise and have Trane adjust how it schedules the exercises accordingly. Students should also be able to make a note of which exercises they need to review with their teachers during their next session.

Using the earlier example of performing a large musical piece, a teacher could ask the student to perform some exercises in front of them and give them a score, which might be different from the student's self-assessment. Teacher and student could also go through the list of exercises the student flagged for review as the teacher provides notes on the finer points of performing the piece.

Using Trane

Trane is split in multiple components for the sake of modularity. They are described below.

  • trane: The core library responsible for opening and interacting with Trane courses. It also includes the data formats for Trane and the utilities to build new courses.
  • trane-cli: A command-line interface for Trane.
  • trane-app: An upcoming graphical interface for Trane.
  • trane-server: An upcoming HTTP REST server for Trane to allow software not written in Rust and software incompatible with the GPLv3 license to use Trane.

trane

trane is the core library responsible for opening and interacting with Trane courses. It also includes the data formats for Trane and the utilities to build new courses.

Other applications are free to integrate Trane in their own ways. For example, a fighting video game could integrate Trane into their tutorial mode to allow players to efficiently practice their skills and make automatic progress in mastering all the game's mechanics.

This library contains utilities to make it easier to write Trane courses. Refer to the section Writing Trane Courses for more information.

trane-cli

trane-cli is the command-line interface for Trane.

Installation instructions

For installation instructions, refer to the quick start guide.

Starting guide

Running the command

To start the binary call trane, if you installed it, or cargo run from the repo's root directory. As of now, the command line does not take any arguments. Once you start the CLI, you will be met with a prompt.

trane >>

Entering enter executes the input command. Pressing CTRL-C cancels the command. Pressing CTRL-D sends an EOF signal to break out of the line reading loop.

Basic shortcuts

  • To execute a command, type the command you want followed by the Enter key.
  • To cancel a command, press CTRL-C.
  • To exit the CLI, press CTRL-D.
  • If the CLI suggests a command, you can press the right arrow key to accept the suggestion.
  • To look through the history of commands, press the up and down arrow keys.
  • To look up a specific command in the history, press CTRL-R and type the command you want to look up. Pressing CTRL-R again will cycle through the commands that match the search.

Entering your first command

To see the next exercise, enter (prompt not shown for brevity) trane next.

Internally, the clap library is being used to process the input. This requires that a command name is present, even though it's redundant because this CLI can only run one command. For this reason, trane-cli automatically prepends the command trane if it's not there already. So all commands can be run without the need for adding trane to the beginning.

Opening a course library

The previous command returns an error because Trane has not opened a course library. A course library is a set of courses under a directory containing a subdirectory named .trane/. Inside this subdirectory, Trane stores the results of previous exercises, blacklists, and saved filters. This directory is created automatically.

Let's suppose you have downloaded the trane-music and called Trane inside that directory. Then, you can type open ./ to load all the library under that directory.

Your first study session

If all the courses are valid, the operation will succeed. Now you can run the next command. Your first exercise should be shown.

trane >> next
Course ID: trane::music::guitar::basic_fretboard
Lesson ID: trane::music::guitar::basic_fretboard::lesson_1
Exercise ID: trane::music::guitar::basic_fretboard::lesson_1::exercise_7

Find the note G in the fretboard at a slow tempo without a metronome.

If you are unsure on what to do, you can try looking at the instructions for this lesson by running the instructions lesson command:

trane >> instructions lesson
Go down each string and find the given note in the first twelve frets.
Repeat but this time going up the strings.

Do this at a slow tempo but without a metronome.

Lessons and courses can also include accompanying material. For example, a lesson on the major scale could include material defining the major scale, and it's basic intervals for reference. This course does not contain any material. For those lessons or courses which do, you can display it with the material lesson and material course commands respectively.

So this exercise belongs to a course teaching the positions of the notes in the guitar fretboard, and it is asking us to go up and down the strings to find the note. Once you have given the exercise a try, you can set your score. There are no objective definitions of which score means but the main difference between them is the degree of unconscious mastery over the exercise. A score of one means you are just learning the position of the note, you still make mistakes, and have to commit conscious effort to the task. A score of five would mean you don't even have to think about the task because it has thoroughly soaked through all the various pathways involved in learning.

Let say we give it a score of two out of five. You can do so by entering score 2. Let's assume that you also want to verify your answer. If there's an answer associated with the current exercise, you can show it by running the answer command:

trane >> answer
Course ID: trane::music::guitar::basic_fretboard
Lesson ID: trane::music::guitar::basic_fretboard::lesson_1
Exercise ID: trane::music::guitar::basic_fretboard::lesson_1::exercise_7

Answer:

- 1st string (high E): 3rd fret
- 2nd string (B): 8th fret
- 3rd string (G): 12th fret
- 4th string (D): 5th fret
- 5th string (A): 10th fret
- 6th string (low E): 3rd fret

To show the current exercise again, you can use the current command. Now it's time to move onto the next question. Questions are presented in the order Trane schedules them and as you master the exercises you automatically unlock new lessons and courses.

Short reference for other commands.

At its simplest, the previous commands cover much of the most common operations. The documentation (accessed with the help or <COMMAND> --help commands) is pretty much self-explanatory for most other commands.

A command-line interface for Trane

Usage: trane <COMMAND>

Commands:
  answer        Show the answer to the current exercise, if it exists
  blacklist     Subcommands to manipulate the unit blacklist
  current       Display the current exercise
  debug         Subcommands for debugging purposes
  filter        Subcommands for dealing with unit filters
  instructions  Subcommands for showing course and lesson instructions
  list          Subcommands for listing course, lesson, and exercise IDs
  mantra-count  Show the number of Tara Sarasvati mantras recited in the background during the current session
  material      Subcommands for showing course and lesson materials
  next          Submits the score for the current exercise and proceeds to the next
  open          Open the course library at the given location
  repository    Subcommands for manipulating git repositories containing Trane courses
  review-list   Subcommands for manipulating the review list
  score         Record the mastery score (1-5) for the current exercise
  search        Search for courses, lessons, and exercises
  scores        Show the most recent scores for the given exercise
  help          Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

There are however, some details which warrant further explanation.

The filter metadata command allows you to define simple metadata filters. For example, to only show exercises for the major scale in the key of C, you can type:

trane >> filter metadata --course-metadata scale_type:major --lesson-metadata key:C
Set the unit filter to only show exercises with the given metadata

The filter set-saved command allows you to user more complex filters by storing the definition of the filter inside the .trane/filters directory. For now, a filter can be created by serializing a struct of type NamedFilter into a JSON file (see the file src/data/filter.rs inside the Trane repo for more details). You can refer to those filters by a unique ID in their file, which can be also shown by running the filter list-saved command.

trane-app

UPCOMING: trane-app is a graphic interface for Trane. It is currently under construction.

trane-server

UPCOMING: trane-server is a server for Trane to allow other applications to use Trane without requiring the use of Rust or for applications incompatible with the GPLv3 License. It is currently under construction.

User Preferences

The user preferences can be set to customize the behavior of Trane.

Specification

The specification for the user preferences:

#![allow(unused)]
fn main() {
/// The user-specific configuration
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct UserPreferences {
    /// The preferences for generating transcription courses.
    #[serde(default)]
    pub transcription: Option<TranscriptionPreferences>,

    /// The preferences for customizing the behavior of the scheduler.
    #[serde(default)]
    pub scheduler: Option<SchedulerPreferences>,

    /// The paths to ignore when opening the course library. The paths are relative to the
    /// repository root. All child paths are also ignored. For example, adding the directory
    /// "foo/bar" will ignore any courses in "foo/bar" or any of its subdirectories.
    #[serde(default)]
    pub ignored_paths: Vec<String>,
}
}

The user preferences can be set in the user_preferences.json file inside the .trane directory in a Trane library. The value is a serialized JSON object that matches the Rust type shown above. If none is found, Trane will create it with the default values.

Currently, the only user preferences that are supported are the ones for the transcription course. Refer to the Transcription Course page for more information on the user preferences for that course.

Saved Filters

A filter restricts the set of exercises that are shown to the user (see the Concepts page for more information on the types of filters).

The user can save a filter for later use by writing it down in a JSON file inside the .trane/filters directory in the library.

The specification for a saved filter is as follows:

#![allow(unused)]
fn main() {
/// A saved filter for easy reference.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SavedFilter {
    /// A unique ID for the filter.
    pub id: String,

    /// A human-readable description of the filter.
    pub description: String,

    /// The filter to apply.
    pub filter: UnitFilter,
}
}

Below there are some examples of saved filters.

A filter to show only exercises for ear training.

{
    "id": "ear_training",
    "description": "Ear traning courses",
    "filter": {
        "MetadataFilter": {
            "filter": {
                "course_filter": {
                    "CombinedFilter": {
                        "op": "Any",
                        "filters": [
                            {
                                "BasicFilter": {
                                    "key": "course_series",
                                    "value": "sing_the_numbers",
                                    "filter_type": "Include"
                                }
                            },
                            {
                                "BasicFilter": {
                                    "key": "course_series",
                                    "value": "progressive_sight_singing",
                                    "filter_type": "Include"
                                }
                            }
                        ]
                    }
                },
                "op": "All"
            }
        }
    }
}

A filter to only show exercises for the guitar.

{
    "id": "guitar",
    "description": "All guitar courses",
    "filter": {
        "MetadataFilter": {
            "filter": {
                "course_filter": {
                    "BasicFilter": {
                        "key": "instrument",
                        "value": "guitar",
                        "filter_type": "Include"
                    }
                },
                "lesson_filter": null,
                "op": "Any"
            }
        }
    }
}

Official Trane Courses

The Trane Project also aims to offer quality official courses to go along with the software. Here's the list of all official courses.

  • trane-music: Courses on music. Still under construction but a lot more courses are in the works.
  • trane-earmaster: Exercises from EarMaster 7 converted into Trane courses. Requires a copy of EarMaster 7.
  • trane-leetcode: Courses on algorithm and data structure problems using exercises from LeetCode.
  • trane-rustlings: A course showcasing how Trane can be used to extend existing educational materials. This course augments rustlings, a course to learn the Rust programming language.

Writing Trane Courses

Trane courses have the following directory structure:

course_root/
    course_manifest.json
    <LESSON_DIR>/
        lesson_manifest.json
        <EXERCISE_DIR>/
            exercise_manifest.json

There can be multiple lessons in a course and multiple exercises in a lesson. In addition, there can be asset files (e.g. markdown files) anywhere in the course directory, but it's best to put each of them in the same directory as the manifest that refers to it.

So writing a course for Trane consists of generating the manifest files in this directory structure and placing any assets in the right paths. While this process is easy for small courses, it can quickly get out of hands if you are generating courses with a lot of exercises or if you are planning on creating a lot of courses.

For this reason, Trane provides utilities to help you write courses inside the course_builder module in Trane. These utilities help you define the courses, lessons, and exercises as Rust code and contain methods to generate all the files (including assets) while also performing several sanity checks. While the decision of generating courses by writing Rust source files might seem strange at first, it's actually a lot easier and less annoying than writing a course by hand. Another advantage is that any breaking change to the manifest data structures in Trane will result in a compile error, so you can be confident that your courses will work with a specific version of Trane as long as the code compiles.

These utilities can be augmented to help you write more easily specific types of courses. For example, inside the course_builder module, there is a CirclesFifthCourse struct that can be used to generate courses that require a lesson for each key and which follow the order of the circles of fifths, starting with the key of C and going clockwise and counterclockwise into each of the other keys.

You can look at any of the official Trane courses for example of how courses are written.

Generated Courses

Writing Trane courses in the way specified in the Writing Trane Courses section can get tedious quickly, even if you are automating the process. In order to make it easier to write courses for Trane, Trane can now automatically generate most of the manifest files specified in the previous section as long as your files fit the structure specified by each type of generated course.

This has several advantages:

  • Reduce the number of manifests needed to write a course. Generated courses still require a course_manifest.json file. This file specifies the type of generated course as well as any available options. However, you do not need to write any manifests for the lessons and exercises in the course.
  • You can combine normal and generated courses seamlessly. Internally, Trane uses the generator config and the files in the course directory to generate all the manifests needed. This means that once those manifests are generated, Trane makes no distinction between normal and generated courses. Generated courses can include individual lessons that are specified via their manifests. Generated courses can depend on any other course or lesson.
  • Less maintenance required for course authors. Trane's data formats and APIs are still subject to change. Since generated courses do not require you to write all but the course manifest, most changes to those formats should not require any changes on your part.
  • Save on disk space. Normal courses can end up with a lot of manifests (one per the course, one per lesson, and another one per exercise). Generated courses only require at minimum one manifest per course but can contain more if there are lessons that are not generated.
  • Generated courses enable custom learning experiences. For example, the transcription course takes a set of musical passages, which could either be entire songs or just a few bars, and generates a course that guides the student through internalizing the song in their ear and their instruments of choice.

How to Write a Generated Course

The process is as follows:

  1. Create a directory for your course. And write the course_manifest.json file just as you would for a normal course. A new field called course_generator specifies which course generator to use for this course. Its value is None for normal courses. For generated courses, it must be specified along with any available options.
  2. Add files to your course directory. Each type of generated course requires a specific type of file structure. Refer to the documentation for each type of generated course for more details.
  3. Once that process is complete, open the Trane library to which this course belongs. All the required manifests will be generated automatically and added to Trane. After that, you can work with them in the same way as you would with any other course.

Generated Course config

The specification for the course_generator field in the course_manifest.json file is as follows:

#![allow(unused)]
fn main() {
/// A configuration used for generating special types of courses on the fly.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum CourseGenerator {
    /// The configuration for generating a knowledge base course. Currently, there are no
    /// configuration options, but the struct was added to implement the [GenerateManifests] trait
    /// and for future extensibility.
    KnowledgeBase(KnowledgeBaseConfig),

    /// The configuration for generating a music piece course.
    MusicPiece(MusicPieceConfig),

    /// The configuration for generating a transcription course.
    Transcription(TranscriptionConfig),
}
}

Refer to the documentation for each type of generated course for the specification for each type of config.

Example configs

Refer to the documentation for each type of generated course for examples of the course_generator configuration.

Knowledge Base Course Generator

This course generator is the most generic of them and can be adopted to create courses for any subject or topic. The idea is to reduce the need to write manifests for the lessons and exercises by giving special meaning to the names of their files and directories. The documentation of the code contains the specification of the file structure that is used to generate the manifests, so it's reproduced below. Note that Vec is a list of values, Ustr is a unique string type that is treated just like a string, a BTreeMap is used to store the metadata as keys and their associated list of values, and Option is used to represent files that are not required.

Here's the specification for an exercise:

#![allow(unused)]
fn main() {
/// Represents a knowledge base exercise.
///
/// Inside a knowledge base lesson directory, Trane will look for matching pairs of files with names
/// `<SHORT_EXERCISE_ID>.front.md` and `<SHORT_EXERCISE_ID>.back.md`. The short ID is used to
/// generate the final exercise ID, by combining it with the lesson ID. For example, files
/// `e.front.md` and `e.back.md` in a course with ID `a::b::c` inside a lesson directory named
/// `d.lesson` will generate and exercise with ID `a::b::c::d::e`.
///
/// Each the optional fields mirror one of the fields in the
/// [ExerciseManifest](crate::data::ExerciseManifest) and their values can be set by writing a JSON
/// file inside the lesson directory with the name `<SHORT_EXERCISE_ID>.<PROPERTY_NAME>.json`. This
/// file should contain a JSON serialization of the desired value. For example, to set the
/// exercise's name for an exercise with a short ID value of `ex1`, one would write a file named
/// `ex1.name.json` containing a JSON string with the desired name.
///
/// Trane will ignore any markdown files that do not match the exercise name pattern or that do not
/// have a matching pair of front and back files.
pub struct KnowledgeBaseExercise {
    /// The short ID of the lesson, which is used to easily identify the exercise and to generate
    /// the final exercise ID.
    pub short_id: String,

    /// The short ID of the lesson to which this exercise belongs.
    pub short_lesson_id: Ustr,

    /// The ID of the course to which this lesson belongs.
    pub course_id: Ustr,

    /// The path to the file containing the front of the flashcard.
    pub front_file: String,

    /// The path to the file containing the back of the flashcard. This path is optional, because a
    /// flashcard is not required to provide an answer.
    pub back_file: Option<String>,

    /// The name of the exercise to be presented to the user.
    pub name: Option<String>,

    /// An optional description of the exercise.
    pub description: Option<String>,

    /// The type of knowledge the exercise tests. Currently, Trane does not make any distinction
    /// between the types of exercises, but that will likely change in the future. The option to set
    /// the type is provided, but most users should not need to use it.
    pub exercise_type: Option<ExerciseType>,
}
}

And here's the specification for a lesson:

#![allow(unused)]
fn main() {
/// Represents a knowledge base lesson.
///
/// In a knowledge base course, lessons are generated by searching for all directories with a name
/// in the format `<SHORT_LESSON_ID>.lesson`. In this case, the short ID is not the entire lesson ID
/// one would use in the lesson manifest, but rather a short identifier that is combined with the
/// course ID to generate the final lesson ID. For example, a course with ID `a::b::c` which
/// contains a directory of name `d.lesson` will generate the manifest for a lesson with ID
/// `a::b::c::d`.
///
/// All the optional fields mirror one of the fields in the
/// [LessonManifest](crate::data::LessonManifest) and their values can be set by writing a JSON file
/// inside the lesson directory with the name `lesson.<PROPERTY_NAME>.json`. This file should
/// contain a JSON serialization of the desired value. For example, to set the lesson's dependencies
/// one would write a file named `lesson.dependencies.json` containing a JSON array of strings, each
/// of them the ID of a dependency.
///
/// The material and instructions of the lesson do not follow this convention. Instead, the files
/// `lesson.instructoins.md` and `lesson.material.md` contain the instructions and material of the
/// lesson.
///
/// None of the `<SHORT_LESSON_ID>.lesson` directories should contain a `lesson_manifest.json` file,
/// as that file would indicate to Trane that this is a regular lesson and not a generated lesson.
pub struct KnowledgeBaseLesson {
    /// The short ID of the lesson, which is used to easily identify the lesson and to generate the
    /// final lesson ID.
    pub short_id: Ustr,

    /// The ID of the course to which this lesson belongs.
    pub course_id: Ustr,

    /// The IDs of all dependencies of this lesson. The values can be full lesson IDs or the short
    /// ID of one of the other lessons in the course. If Trane finds a dependency with a short ID,
    /// it will automatically generate the full lesson ID. Not setting this value will indicate that
    /// the lesson has no dependencies.
    pub dependencies: Vec<Ustr>,

    /// The IDs of all courses or lessons that this lesson supersedes. Like the dependencies, the
    /// values can be full lesson IDs or the short ID of one of the other lessons in the course. The
    /// same resolution rules apply.
    pub superseded: Vec<Ustr>,

    /// The name of the lesson to be presented to the user.
    pub name: Option<String>,

    /// An optional description of the lesson.
    pub description: Option<String>,

    //// A mapping of String keys to a list of String values used to store arbitrary metadata about
    ///the lesson. This value is set to a `BTreeMap` to ensure that the keys are sorted in a
    ///consistent order when serialized. This is an implementation detail and does not affect how
    ///the value should be written to a file. A JSON map of strings to list of strings works.
    pub metadata: Option<BTreeMap<String, Vec<String>>>,

    /// Indicates whether the `lesson.instructions.md` file is present in the lesson directory.
    pub has_instructions: bool,

    /// Indicates whether the `lesson.material.md` file is present in the lesson directory.
    pub has_material: bool,
}
}

If we translate this specification into an example course with two lessons of two exercises each, the file structure is as follows:

course_root/
├── course_manifest.json
├── lesson_1.lesson/
|   ├── lesson.name.json
|   ├── lesson.description.json
|   ├── lesson.dependencies.json
|   ├── lesson.metadata.json
|   ├── lesson.instructions.md
|   ├── lesson.material.md
│   ├── exercise_1.front.md
│   ├── exercise_1.back.md
│   ├── exercise_1.name.json
│   ├── exercise_1.description.json
│   ├── exercise_1.type.json
│   ├── exercise_2.front.md
│   └── exercise_2.back.md
│   ├── exercise_2.name.json
│   ├── exercise_2.description.json
│   ├── exercise_2.type.json
└── lesson_2.lesson/
    ├── exercise_3.front.md
    ├── exercise_3.back.md
    ├── exercise_4.front.md

The first lesson includes all the files that can be written to specify the properties of a lesson, the front, and back of exercises, and the properties of each exercise. The second lesson contains the minimum number of files required to specify the lesson and exercises. Not even the dependencies of a lesson are required. If they are missing, the lesson will be assumed to have no dependencies. The rest of the properties are given a sensible default. Also note that an exercise is not required to have a back file, such as exercise_4 in the example above. This can happen, for example, if the exercise is open-ended or refers to an external resource.

Most of the time, a course author will only need to specify the front and back of the exercises and the dependencies for each lesson. The flashcards already needed to be written anyway, and the dependencies are much easier to write than the entire manifest. Following this strategy, most of the extra effort to writing the course materials themselves that was previously required to complete the course is eliminated.

Example Configuration

Example of a course manifest for a knowledge base course:

{
  "id": "trane::example::knowledge_base",
  "name": "Example Knowledge Base Course",
  "dependencies": [],
  "description": "An example knowledge base course.",
  "authors": [
    "The Trane Project"
  ],
  "metadata": null,
  "course_material": null,
  "course_instructions": null,
  "generator_config": {
    "KnowledgeBase": {}
  }
}

Example Courses

Below there are a few examples of official Trane courses that are using this course generator to give you a better idea of how it is used:

  • Tenor saxophone long tones: A course to teach you how to produce long tones on the tenor saxophone, starting at the first octave and gradually working your way up.
  • Art of Problem Solving 1: The "Art of Problem Solving" book series is intended to begin preparing students for competing in math Olympiads. This course translates the first volume of that series into a Trane course.

Simple Course Builder

For simple courses that mostly have flashcards with a front and back, it's possible to declare the entire course in a single file with the help of the SimpleKnowledgeBaseCourse object. Simply write a JSON file that is the serialization of that type and call:

trane-simple-build <PATH_TO_FILE> <OUTPUT_DIR>

The trane-simple-build utility is found in the trane-cli repository. You can install it by cloning that repository and running the following command at the root of the repository:

cargo install --path .

Note that this assumes that you have installed the Rust toolchain. If you haven't, you can install it by following the instructions in the Rust website.

Below is the specification of the SimpleKnowledgeBaseCourse type:

#![allow(unused)]
fn main() {
/// Represents a simple knowledge base course which only specifies the course manifest and a list of
/// simple lessons. It is meant to help course authors write simple knowledge base courses by
/// writing a simple configuration to a single JSON file.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimpleKnowledgeBaseCourse {
    /// The manifest for this course.
    pub manifest: CourseManifest,

    /// The simple lessons in this course.
    #[serde(default)]
    pub lessons: Vec<SimpleKnowledgeBaseLesson>,
}
}

and the specification of the SimpleKnowledgeBaseLesson type referenced by the course type:

#![allow(unused)]
fn main() {
/// Represents a simple knowledge base lesson which only specifies the short ID of the lesson, the
/// dependencies of the lesson, and a list of simple exercises. The instructions, material, and
/// metadata can be optionally specified as well. In a lot of cases, this is enough to deliver the
/// full functionality of Trane. It is meant to help course authors write simple knowledge base
/// courses by writing a simple configuration to a single JSON file.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SimpleKnowledgeBaseLesson {
    /// The short ID of the lesson.
    pub short_id: Ustr,

    /// The dependencies of the lesson.
    #[serde(default)]
    pub dependencies: Vec<Ustr>,

    /// The courses or lessons that this lesson supersedes.
    #[serde(default)]
    pub superseded: Vec<Ustr>,

    /// The simple exercises in the lesson.
    #[serde(default)]
    pub exercises: Vec<SimpleKnowledgeBaseExercise>,

    /// The optional metadata for the lesson.
    #[serde(default)]
    pub metadata: Option<BTreeMap<String, Vec<String>>>,

    /// A list of additional files to write in the lesson directory.
    #[serde(default)]
    pub additional_files: Vec<AssetBuilder>,
}
}

Examples

The specification above might not be very clear, specially if you don't know Rust, so here are a few examples of existing courses that use this builder:

  • trane-n-back: A course that trains your working memory by playing the n-back game.
  • Tenor saxophone long tones: This course was mentioned above and while it's a valid knowledge base course, it's generated from this file.

Usually when generating those courses, the following commands are used:

cd <DIRECTORY_OF_COURSE_CONFIG_FILE>
trane-simple-build course_config.json .

Running the command generates all the lesson directories, lesson metadata files, and exercise files. The changes are then committed to the repository.

Music Piece Generator

This course generator is used to create courses that progressively teach mastery of a musical piece, starting at the smallest fragments and gradually building up to the entire piece. The course author must divide up the piece into fragments and write them down in the configuration of the course.

Specification

The specification for the music asset containing the music sheet of the piece:

#![allow(unused)]
fn main() {
/// Represents a music asset to be practiced.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum MusicAsset {
    /// A link to a SoundSlice.
    SoundSlice(String),

    /// The path to a local file. For example, the path to a PDF of the sheet music.
    LocalFile(String),
}
}

The specification for how musical passages are declared and divided up:

#![allow(unused)]
fn main() {
/// Represents a music passage to be practiced.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct MusicPassage {
    /// The start of the passage.
    pub start: String,

    /// The end of the passage.
    pub end: String,

    /// The sub-passages that must be mastered before this passage can be mastered. Each
    /// sub-passage should be given a unique index which will be used to generate the lesson ID.
    /// Those values should not change once they are defined or progress for this lesson will be
    /// lost. This value is a map instead of a list because rearranging the order of the
    /// passages in a list would also change the IDs of the generated lessons.
    pub sub_passages: HashMap<usize, MusicPassage>,
}
}

The specification for the course configuration for music piece courses:

#![allow(unused)]
fn main() {
/// The config to create a course that teaches a piece of music.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct MusicPieceConfig {
    /// The asset containing the music to be practiced.
    pub music_asset: MusicAsset,

    /// The passages in which the music is divided for practice.
    pub passages: MusicPassage,
}
}

The top musical passage should always ask the student to play the entire piece. Each of the dependencies asks the student to play a smaller fragment of the piece. There can be an arbitrary number of nested passages, which allow pieces to be divided into arbitrarily small fragments. To allow more flexibility, the start and end values of each passage are specified as a string.

The musical asset specifies where the music sheet for the piece is located. It can be specified as a link to a SoundSlice or as the path to a local file.

Example configuration

Below is an example course generator config for this type of course.

{
  "id": "trane::example::knowledge_base",
  "name": "Example Knowledge Base Course",
  "dependencies": [],
  "description": "An example knowledge base course.",
  "authors": [
    "The Trane Project"
  ],
  "metadata": null,
  "course_material": null,
  "course_instructions": null,
  "generator_config": {
    "MusicPiece": {
      "music_asset": {
        "LocalFile": "music_sheet.pdf"
      },
      "passages": {
        "start": "start of the piece",
        "end": "end of the piece",
        "sub_passages": [
          {
            "start": "start of bar 1",
            "end": "end of bar 10",
            "sub_passages": []
          },
          {
            "start": "start of bar 10",
            "end": "end of bar 20",
            "sub_passages": []
          }
        ]
      }
    }
  }
}

Example course

TODO: This section is under construction.

Transcription Course Generator

The transcription course generator is used to create courses that help you learn how to transcribe passages of music onto your ear and your instrument(s). This page explains how they are configured and set up. For more information on the methodology and instructions, see the course instructions page.

Specification

The specification for the course configuration for transcription courses:

#![allow(unused)]
fn main() {
/// The configuration used to generate a transcription course.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct TranscriptionConfig {
    /// The dependencies on other transcription courses. Specifying these dependencies here instead
    /// of the [CourseManifest](crate::data::CourseManifest) allows Trane to generate more
    /// fine-grained dependencies.
    #[serde(default)]
    pub transcription_dependencies: Vec<Ustr>,

    /// The directory where the passages are stored as JSON files whose contents are serialized
    /// [TranscriptionPassages] objects.
    ///
    /// The directory can be written relative to the root of the course or as an absolute path. The
    /// first option is recommended. An empty value will safely default to not reading any files.
    #[serde(default)]
    pub passage_directory: String,

    /// A list of passages to include in the course in addition to the ones in the passage
    /// directory. Useful for adding passages directly in the course manifest.
    #[serde(default)]
    pub inlined_passages: Vec<TranscriptionPassages>,

    /// If true, the course will skip creating the singing lesson. This is useful when the course
    /// contains backing tracks that have not melodies, for example. Both the singing and the
    /// advanced singing lessons will be skipped. Because other transcription courses that depend on
    /// this lesson will use the singing lesson to create the dependency, the lesson will be
    /// created, but will be empty.
    #[serde(default)]
    pub skip_singing_lessons: bool,

    /// If true, the course will skip the advanced singing and transcription lessons. This is useful
    /// when there are copies of the same recording for every key, which makes the need for the
    /// advanced lessons obsolete.
    #[serde(default)]
    pub skip_advanced_lessons: bool,
}
}

The specification for how the passages are declared:

#![allow(unused)]
fn main() {
/// A collection of passages from a track that can be used for a transcription course.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct TranscriptionPassages {
    /// The asset to transcribe.
    pub asset: TranscriptionAsset,

    /// The ranges `[start, end]` of the passages to transcribe. Stored as a map maping a unique ID
    /// to the start and end of the passage. A map is used to get the indices instead of getting
    /// them from a vector because reordering the passages would change the resulting exercise IDs.
    pub intervals: HashMap<usize, (String, String)>,
}
}

The specification for how the transcription assets (the description of the song to transcribe) are declared:

#![allow(unused)]
fn main() {
/// An asset used for the transcription course generator.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum TranscriptionAsset {
    /// A track of recorded music that is not included along with the course. Used to reference
    /// commercial music for which there is no legal way to distribute the audio.
    Track {
        /// A unique short ID for the asset. This value will be used to generate the exercise IDs.
        short_id: String,

        /// The name of the track to use for transcription.
        track_name: String,

        /// The name of the artist(s) who performs the track.
        #[serde(default)]
        artist_name: Option<String>,

        /// The name of the album in which the track appears.
        #[serde(default)]
        album_name: Option<String>,

        /// The duration of the track.
        #[serde(default)]
        duration: Option<String>,

        /// A link to an external copy (e.g. youtube video) of the track.
        #[serde(default)]
        external_link: Option<TranscriptionLink>,
    },
}
}

The specification for how the external links are declared:

#![allow(unused)]
fn main() {
/// A link to an external resource for a transcription asset.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum TranscriptionLink {
    /// A link to a YouTube video.
    YouTube(String),
}

impl TranscriptionLink {
    /// Returns the URL of the link.
    pub fn url(&self) -> &str {
        match self {
            TranscriptionLink::YouTube(url) => url,
        }
    }
}
}

The transcription courses allow you to specify which instruments you want to use for the courses in the user preferences. Refer to the User Preferences page for more details.

The specification for the user preferences for the transcription course is as follows:

#![allow(unused)]
fn main() {
/// Settings for generating a new transcription course that are specific to a user.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct TranscriptionPreferences {
    /// The list of instruments the user wants to practice. Note that changing the instrument ID
    /// will change the IDs of the exercises and lose your progress, so it should be chosen
    /// carefully before you start practicing.
    #[serde(default)]
    pub instruments: Vec<Instrument>,
}
}

These preferences require that you declare the instruments that you want to use for the transcription. The specification for them is:

#![allow(unused)]
fn main() {
/// Describes an instrument that can be used to practice in a generated course.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Instrument {
    /// The name of the instrument. For example, "Tenor Saxophone".
    pub name: String,

    /// An ID for this instrument used to generate lesson IDs. For example, "tenor_saxophone".
    pub id: String,
}
}

Example courses

Here are a few examples of existing transcription courses:

Transcription Course Instructions

This course is designed to help you learn how to transcribe passages of music onto your ear and your instrument(s). The aim of the exercises is not to notate the music, but to learn how to extract the musical elements of the passage into your preferred instruments, and ingrain them in your playing. Students that wish to notate the music are free to do so, but it is not required to make progress in the course.

The goal of these courses is to present a similar experience to the apprenticeship model on which traditional African music and jazz was taught. In this model, the master shows the apprentice a musical passage, and the apprentice must learn how to reproduce it. Unlike a school or academy, graduation from the apprenticeship model is not based on completing a fixed curriculum, but on actual mastery of the material.

How to use the course

The questions in the course refer you to the passage that you must transcribe. Due to copyright laws, an audio file of the passage is not provided. Instead, you are given the passage as the information about the track (artist, album, and track number) and the start and end timestamps of the passage (which could just say to play over the entire song). There is the option to give an optional external link, containing a link to the YouTube video of the track, for example.

You then get a copy of the track through your own means, load the track into your preferred music player (see the section on tools for the recommended player), and find the passage in the track. You then proceed according to the stage indicated by the exercises.

Lessons

The course is divided in several lessons, each of which will help you become familiar with a different aspect of the material.

Singing

First listen to the musical passage until you can audiate it clearly in your head. Then sing over the passage. At this stage it's not required to be accurate as possible. Rather, learn to sing the main elements of the passage and experiment with different melodies over it. The goal is to learn to navigate the context implied by the passage.

Transcription

With the basic context implied by the passage now internalized in your ear, try to play over it using your instrument. The goal at this point is not to accurately reproduce the passage, but rather about learning to navigate that context and use it as a basis for improvisation. You can focus on different elements or sections each time you practice.

The lesson depends on sufficiently mastering the singing lesson.

Advanced Singing

At this stage, you can sing and play over the context implied by the passage, and sing it with more detail and precision in a variety of keys. It's at this point that you can engage in what is traditionally called transcription.

Play over the passage using your instrument, and try to reproduce it in more detail than in the basic transcription lesson. You should also transpose the passage up or down a random number of semitones. You should still use the passage as a basis for improvisation, but the focus is much narrower than in the basic transcription lesson, and the actual music played in the passage take precedence over the context implied by it.

The lesson depends on sufficiently mastering the singing lesson.

Advanced Transcription

At this stage, you can sing and play over the context implied by the passage, and sing it with more detail and precision in a variety of keys. It's at this point that you can engage in what is traditionally called transcription.

Play over the passage using your instrument, and try to reproduce it in more detail than in the basic transcription lesson. You should also transpose the passage up or down a random number of semitones. You should still use the passage as a basis for improvisation, but the focus is much narrower than in the basic transcription lesson, and the actual music played in the passage take precedence over the context implied by it.

The lesson depends on sufficiently mastering the advanced singing lesson and the transcription lesson.

Tools

Transcribe!

The recommended tool for these exercises is Transcribe!. This is proprietary and paid software, but is the best option for helping you transcribe the passages in these courses. It can slow down, loop, and transpose the audio. It also has built-in tools to help you find which notes and chords are being played.

There exists similar software, although not as feature-rich as Transcribe. At the very least, you should be able to easily loop, slow down, and transpose the audio.

Licensing

All code in Trane is licensed under the GPLv3 license with one exception. For projects with an incompatible license, the upcoming MIT-licensed trane-server project will allow them to interact with Trane without having to release their code under the GPLv3 license.

Contributing to Trane

Trane is accepting contributions, specially for trane-music. Here's a list of ways to contribute:

Code contributions

Trane and its associated projects are still in a very early stage, so I am not looking for code contributions at this time. However, feel free to open an issue if you notice a bug or an issue with my usage of Rust, since I am new to the language.

Suggesting new material in one of the official courses

Easiest way to contribute is by simply opening an issue in the appropriate repo and suggesting new courses or lessons to be added. It is especially useful to detail how the material in those units relates to other material (either in existing courses or ones that have not been added yet) and how you would break the material into smaller lessons, if it is necessary. Either links to reference material or example exercises are also useful.

If you are a domain expert but can't (or want) to code any material yourself, this is a great way to contribute as a large part of the task at hand is breaking down skills and knowledge into smaller units and figuring a sensible order in which they should be introduced to students.

Planning new material

This is a more involved version of the above. You still don't need to code any material yourself but if you already know exactly how the material you want to add translates to courses, lessons, and exercises, you can write them in a simplified format in plain text and propose the course is added by opening and issue and sharing your work. Once all the details have been ironed out and your proposal has been accepted, someone else can work into translating it into working code.

Coding new material

You can also contribute by adding the code to generate new courses and lessons. The best way to learn is to look at how existing courses are defined. You can either create courses for material you have designed yourself or generate new material that has been proposed and approved but still needs to be added as code. Make sure you have gotten an approval to work on new material before submitting a PR.

FAQ

What is the current state of the project?

Trane is in an early state, but it works and should not break for most people. A lot of bugs were found and addressed in earlier versions in the process of testing the library and reaching 100% code coverage (see the blog post). If you run into any bugs, feel free to report them in the relevant GitHub repository.

Due to the early state of the project, Trane is subject changes to its API and data formats. The only state by Trane depends on the ID of a unit, so as long as that is not changed, updating the files to match any breaking changes is the only thing needed to make Trane pick up the updated versions and keep your progress.

I do not expect big changes in the scheduling logic until there is enough user feedback to know what works and what does not. So far, I've only had to change the values of certain constants because initially progress through the units was a little slow.

How well does Trane work?

The honest answer is that I am not sure. One of the goals of releasing Trane to the public is to get feedback and user reports in the hope that I can fine-tune it. I suspect Trane will work fairly well in learning skills that require the repetition of complex chains of patterns until mastery of each and the whole is achieved. Playing music mostly follows this pattern, but I would like to figure out how it can be applied to other skills.

In my personal use, I have found Trane to work well. It is easy to practice for longer than I used to and if I can move on to practice other skills if I encounter an exercise that is too difficult and becomes frustrating. I have made more progress going through the exercises in EarMaster (with the help of trane-earmaster) than I ever did without Trane. However, I am still working regularly on Trane itself and in creating new material for it, so my practice time is limited at the moment.

How do I use Trane?

See the quick start guide for instructions on how to get started.

How do I get content for Trane.

For official courses, see the section on Official Courses.

Since Trane courses are just collections of plain-text files, you can also create your own content. This content can freely reference other courses, even those written by others. For example, you could add new courses that link to a course in trane-music, or add additional exercises to one of the lessons. See the section on writing Trane courses for more information.

You can also experiment with augmenting existing educational materials by translating them into Trane courses. The trane-earmaster course mentioned above is an example of this. It might not be possible at the moment to embed the material directly within Trane, but you can create flashcards that direct you to separate material. For example, you could create a flashcard that asks you to practice a certain section from a PDF music score you own or a flashcard to review a theorem from a math textbook.

Are there plans to have a graphic interface?

I recognize the importance of having a graphical interface and the barrier it creates for less technical users, so it is on my plans to create one eventually. At the moment, I am focusing on the core library and the command-line interface. There are some problems with creating a graphical interface:

  • I am not very familiar with modern GUI or web development. I can learn it, but it would take time away from working on the core library, which I do not have as I work a full-time job.
  • At this moment, the Rust ecosystem for GUI development is not very mature. There are some promising projects, but they are still in an early stage. I would like to wait until they are more mature before committing to a particular one. If I were to use HTML/CSS/JS, I would have to figure out a way to integrate it with the Rust data structures, which is a problem of its own.

Why not Anki or another existing software?

Originally, I tried to use Anki for practicing music but quickly found some limitations. First, Anki and similar software are optimized for memorization, not for practicing skills. Most importantly, defining arbitrary dependencies between subsets of flashcards and having the algorithm use those dependencies to select the flashcards to present is not supported.

The solution given by Anki is to create multiple decks. However, asking users to manually decide which deck to practice and which decks should be practiced once the current one is sufficiently mastered sort of defeats the purpose of using an automated system in the first place.

I tried looking for other software that would allow me to define arbitrary dependencies between flashcards. While some allow a limited version of this, none of them worked the way I envisioned and most were focused on memorization. Out of this frustration, Trane was born.

Development Blog

This is the developer blog for the Trane project. It is meant for sharing content that does not fit well into the main documentation or as a comment in the source code.

Blogs are ordered by the date of original publication.

Semi-Literate Programming

One of the goals of Trane is to make its source code as clear and easy to understand as possible. The reasons are both altruistic and selfish. The altruistic reason is that a clearer codebase would make it easier for potential collaborators to get started, either modifying Trane itself or writing courses for it. The selfish reason is that while I don't expect any monetary reward for releasing my work as free software, I want Trane to serve as a showcase of my programming skills. All in the hopes of never having to solve another algorithm and data structure problem during a job search.

It was with great interest then that I came across the concept of literate programming. The idea and naming comes from Donald Knuth, and at its core it's the idea of writing programs with the human reader at mind, rather than the computer that will execute it. The writing can then follow a more free-flowing narrative that is not constricted by the syntax of the programming language. Immediately I was sold on the idea for a few reasons.

  1. Unlike what seems the vast majority of programmers, I greatly enjoy writing.
  2. Freeing the writing from the syntax would make it easier to guide readers not only through the code but through the reasoning and restrictions that led to its design.
  3. Having to flesh out the ideas along writing the code might help me avoid mistakes in tricky parts of the programs, as Donald Knuth argues so himself.

However, the excitement was short-lived. I quickly realized that the tooling suffers from two issues that make it a non-starter.

  1. The tools are fragmented and there's no clear standard. They are all based on wrapping the code using special syntax and using a pre-processing tool to generate the code that is eventually passed to the compiler or interpreter.
  2. Because of that, all the existing tooling for the language breaks. There's no way to rename a function for example, as the tooling does not understand the special syntax of the tool.

Determined to make the basic ideas behind literate programming work, I decided that the best way forward was to use the existing tooling of the language to write the documentation without the use of any external tool. I call this semi-literate programming. Since Trane is written in Rust, the documentation tooling already supports markdown, references, and links, among other features. It's more than powerful enough, but the only hard requirement is to provide a syntax to write comments, which most languages do.

Below I present some of the guidelines I used to improve Trane's documentation.

Rules of Semi-Literate Programming

  • This shouldn't even have to be said, but comments should be written in clear language free of grammar and spelling mistakes.
  • Comments explaining the why of a section of code are more important than comments answering what or how. However, there's still use for the latter. They allow a reader to quickly skim what a section of code does without having to read a line of code. Below is an example from Trane's scheduler module.
#![allow(unused)]
fn main() {
    /// Returns an initial stack with all the starting units in the graph that are used to search
    /// the entire graph.
    fn get_initial_stack(&self, metadata_filter: Option<&KeyValueFilter>) -> Vec<StackItem> {
        // First get all the starting units and then all of their starting lessons.
        let starting_units = self.get_all_starting_units();
        let mut initial_stack: Vec<StackItem> = vec![];
        for course_id in starting_units {
            // Set the depth to zero since all the starting units are at the same depth.
            let lesson_ids = self
                .get_course_valid_starting_lessons(&course_id, 0, metadata_filter)
                .unwrap_or_default();

            if lesson_ids.is_empty() {
                // For units with no lessons, insert the unit itself as a starting unit so that its
                // dependents are traversed.
                initial_stack.push(StackItem {
                    unit_id: course_id,
                    depth: 0,
                });
            } else {
                // Insert all the starting lessons in the stack.
                initial_stack.extend(
                    lesson_ids
                        .into_iter()
                        .map(|unit_id| StackItem { unit_id, depth: 0 }),
                );
            }
        }

        // Shuffle the lessons to follow a different ordering each time a new batch is requested.
        initial_stack.shuffle(&mut thread_rng());
        initial_stack
    }
}
  • Each file (or module, package, etc. depending on the language) should have a top-level comment that explains the purpose of the file and how it fits into the larger system. This comment should not explain the code in any or too much detail but present the reader with an account of the main design decisions that lead to structuring the code in this way. Below is an example from Trane's scheduler::cache module.
#![allow(unused)]
fn main() {
//! Defines a cache that is used to retrieve unit scores and stores previously computed exercise and
//! lesson scores
//!
//! During performance testing, it was found that caching scores scores significantly improved the
//! performance of exercise scheduling.
}
  • The main file (or module, package, etc.) should explain what the whole library or program does and also contain a short explanation of the structure of the code to allow readers to better navigate the codebase. Below is the relevant section from Trane's lib.rs file:
#![allow(unused)]
fn main() {
//! Here's an overview of some of the most important modules in this crate and their purpose:
//! - [data](crate::data): Contains the basic data structures used by Trane.
//! - [graph](crate::graph): Defines the graph used by Trane to list the units of material and the
//!   dependencies among them.
//! - [course_library](crate::course_library): Reads a collection of courses, lessons, and exercises
//!   from the file system and provides basic utilities for working with them.
//! - [scheduler](crate::scheduler): Defines the algorithm used by Trane to select exercises to
//!   present to the user.
//! - [practice_stats](crate::practice_stats): Stores the results of practice sessions for use in
//!   determining the next batch of exercises.
//! - [blacklist](crate::blacklist): Defines the list of units the student wishes to hide, either
//!   because their material has already been mastered or they do not wish to learn it.
//! - [scorer](crate::scorer): Calculates a score for an exercise based on the results and
//!   timestamps of previous trials.
}
  • Document what each struct, enum, field, function, etc. does, even if it seems obvious to you. It's hard to gauge what will be obvious to the readers of your code, which include future you. If the purpose is obvious, no more than a short sentence is needed. If there's a detail which is not obvious, document why that is so. Below there are examples of both situations taken from Trane's data module.
#![allow(unused)]
fn main() {
/// The result of a single trial.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ExerciseTrial {
    /// The score assigned to the exercise after the trial.
    pub score: f32,

    /// The timestamp at which the trial happened.
    pub timestamp: i64,
}
}
#![allow(unused)]
fn main() {
    //// A mapping of String keys to a list of String values. For example, ("genre", ["jazz"]) could
    /// be attached to a course named "Basic Jazz Chords on Guitar".
    ///
    /// The purpose of this metadata is to allow students to focus on more specific material during
    /// a study session which does not belong to a single lesson or course. For example, a student
    /// might want to only focus on guitar scales or ear training.
    #[builder(default)]
    #[serde(default)]
    pub metadata: Option<BTreeMap<String, Vec<String>>>,

}
  • Document situations in which the design was changed and the reasons why. This is part of using the documentation of your code to tell the story behind the design decisions. Below is an example from Trane's practice_stats module.
#![allow(unused)]
fn main() {
            // Originally the trials were indexed solely by `unit_uid`. This index was replaced so
            // this migration is immediately canceled by the one right below. They cannot be removed
            // from the migration list without breaking databases created in an earlier version than
            // the one which removes them, so they are kept here for now.
            M::up("CREATE INDEX unit_scores ON practice_stats (unit_uid);")
                .down("DROP INDEX unit_scores"),
            M::up("DROP INDEX unit_scores")
                .down("CREATE INDEX unit_scores ON practice_stats (unit_uid);"),
}

Tools for semi-literate programming

Here are a few of the tools that helped me while revamping the documentation for Trane.

  • An editor plugin to auto-wrap comment lines as you type. I develop in Visual Studio Code and use the Rewrap extension.
  • An editor plugin to check the grammar and spelling of comments. I use the LTeX plugin that integrates VS Code with LanguageTool.

Results

Does this level of documentation work to improve the quality of the code? I think so. I started after Trane was already in a fairly complete state, so I cannot say whether this makes writing a new complex codebase from start any easier. However, I can say that I found several minor bugs and did some minor refactoring while in the process of improving the documentation.

Does it work to make it easier for others to understand and contribute to the code? I suppose that judgement falls on others. Whether it can be made to work for larger teams is also a question I cannot answer as Trane is currently a solo project.

Reaching 100% Code Coverage in Rust

As of commit aa3536d0, Trane can boast a code coverage score of 100%. I had difficulty finding a comprehensive guide when I was getting started, so this post is meant to explain how I got there, the tools I used, the problems I ran into, and the lessons I learned. Hopefully this will save time to someone else who is trying to add test coverage to a Rust project.

The Tools

I used grcov to generate the coverage information. This requires that the tests are run in the nightly version of Rust with special flags that create the coverage information. I used the grcov GitHub Action to generate the report. This is a fork of the original repository, which appears to be abandoned. The fork is needed to support excluding lines from the coverage report (more on that later).

For comparing the coverage reports I used Coveralls. This is a free service for open-source repositories that provides a web interface for comparing the reports between commits. Here's the example report of the commit mentioned above. I used the official Coveralls GitHub Action to upload the report to the service.

Here are the configuration files for reference:

Configuring grcov

I ran into multiple issues with grcov which required some configuration to work around them.

  • The initial report included coverage information for all the dependencies. Figuring out the fix for this issue took me a while, but I fixed by setting the ignore option in the configuration file to exclude any directory with .cargo in its path.
  • The initial reports included branch coverage information. I decided to stick initially to line coverage, as I could not get it working with the fix to the previous issue. This required that I set the branch option to false in the configuration file. I might revisit this decision later and try to get branch coverage working.
  • Some lines were marked as not covered, even though they don't contain actual code. They are ignored by setting the excl-line in the configuration file.
    • Some files have the first line marked as not covered. All my files have module-level comments, so I fixed the issue by excluding all lines that start with //! from the coverage report.
    • #[derive... lines are marked as not covered. This is because the code generated by those macros is not covered. Given that I trust the compiler to generate the correct code, I decided to ignore those lines.

Other issues

I ran into an issue in which fields in a struct or some variants of an enum would appear as not covered in the Coveralls report. This is related to the issue with #[derive...] lines. In particular, if a type implements the Clone trait, but the struct or some variant of an enum is not cloned anywhere in the code, some fields will appear as not covered. The fix is to remove the Clone trait if you are not using it or add tests that clone the type if it's an enum and not all variants are cloned in the code.

Some other issues with partial hits using grcov and Rust have been reported. See this issue for more details.

There's also the problem that grcov does not appear to be maintained at the moment, apart from automated commits updating its dependencies. I do not know whether the software has been abandoned or is mostly complete at this point. However, issues have not been addressed in a while, which would indicate that the latter option is more likely.

Getting to 100% Code Coverage

In this section, I will go over some of the steps I took to get to 100% code coverage. The first step was configuring grcov to exclude any arbitrary line or section from the coverage report. This is done in two steps.

The first step was to modify the configuration file to exclude any line with the grcov-excl-line string from the final results. This allows me to exclude lines that are hard to test or not very relevant from the coverage report. For example:

  • The modules that use SQLite call a method to prepare a statement. This is a fallible operation, but this particular line will never fail because the statement does not change. If a bad change to the statement were introduced, the tests would fail anyway. Here's an example from the practice_stats module, which stores the results of the user's practice sessions:
    #![allow(unused)]
    fn main() {
    // Retrieve the exercise trials from the database.
    let connection = self.pool.get()?;
    let mut stmt = connection
        .prepare_cached(
            "SELECT score, timestamp from practice_stats WHERE unit_uid = (
              SELECT unit_uid FROM uids WHERE unit_id = ?1)
              ORDER BY timestamp DESC LIMIT ?2;",
        )
        .with_context(|| "cannot prepare statement")?; //grcov-excl-line
    }
    
  • Adding an entry to the search index is also a fallible operation, but there are already tests that verify that search work, so there's little value in having a test that triggers this error. This is the code from the course_library module, which opens the courses stored in the file system for use with Trane:
    #![allow(unused)]
    fn main() {
      // Add the lesson manifest to the search index.
      Self::add_to_index_writer(
          index_writer,
          lesson_manifest.id,
          &lesson_manifest.name,
          &lesson_manifest.description,
          &lesson_manifest.metadata,
      )?; // grcov-excl-line
    }
    

The second step was to configure grcov to exclude entire sections of code from the report. For example, the Trane struct that is the main entry point for the library reimplements all the interfaces so that users do not have to access the fields of the struct to call those methods. Thus, those lines appeared as uncovered in the report but are actually covered by the all the other tests. I fixed this by configuring the excl-start and excl-stop options to exclude sections of the code from the final report. Here's an example of some of the code that was excluded:

#![allow(unused)]
fn main() {
impl ReviewList for Trane {
    fn add_to_review_list(&mut self, unit_id: &Ustr) -> Result<()> {
        self.review_list.write().add_to_review_list(unit_id)
    }

    fn remove_from_review_list(&mut self, unit_id: &Ustr) -> Result<()> {
        self.review_list.write().remove_from_review_list(unit_id)
    }

    fn all_review_list_entries(&self) -> Result<Vec<Ustr>> {
        self.review_list.read().all_review_list_entries()
    }
}
}

Once those two steps were configured, the coverage was around 90%. The remaining 10% consisted of edge cases that had not been tested yet. For example, there was no test to ensure that trying to open a Trane library with an invalid path would fail. Below is a list of example commits that introduce additional tests to cover those edge cases:

There are about a dozen more commits that gradually took the coverage to 100%. Finally, the commit linked at the top of the post was the first one to reach 100% code coverage. This commit adds a test to verify that the score cache does not return a score for courses that have all of its lessons added to the blacklist.

Conclusion

The effort to get to 100% code coverage was not very time-consuming and uncovered some minor bugs that had gone unnoticed. However, the core of the logic was already covered by existing tests and none of the bugs that were uncovered were critical. Still, I think the effort has given me more confidence to make changes to the code base without worrying about introducing new bugs or modifying existing behavior.

A Love Supreme

This album is a humble offering to Him. An attempt to say "THANK YOU GOD" through our work, even as we do in our hearts and with our tongues. May He help and strengthen all men in every good endeavor.

  • A Love Supreme liner notes

Trane and all associated projects are dedicated to the memory of John Coltrane. Trane is free software as are all of its official courses and the authors are committed to keep it so in the same spirit of love and generosity of his music. May all users of Trane learn and master the skills they desire for the benefit of all beings.