2. Harmonies

Preparing Your Computer

While there’s no shortage of online tutorials explaining how to set up Python on your machine, this guide will simplify the process, focusing on the essentials of starting up with Djalgo, with an introduction to Python and how to use it as a tool for music composition.

Leveraging Cloud Computing

Your web browser and a service provider can be your music composing tool. Navigate to a cloud computing service, sign up for a free plan, and you’re a couple of steps away from being a music coder. Platforms like Deepnote and Google Colab let you write code, display musical scores, and play MIDI files directly from a notebook-style interface. Bear in mind, though, that free plans generally come with service-specific limitations. While processor and RAM limitations won’t hinder simple music composition methods, more advanced techniques like machine learning-based ones could experience a slower workflow. In any case, opening your pockets to a service you love can be rewarding, or course for your service provider, but also for you.

Despite these limitations, Deepnote’s default computing environments, and to a lesser extent Colab’s, both come with a range of handy data science tools, although you will need to install a specific application and a few Python packages at the beginning of each session.

Deepnote and Colab both work on the background with Debian Linux containers. To access to the system, you just add a ! before the command. For instance, you’ll have to access to the system to install MuseScore, an application for writing, visualizing and sharing musical scores. We will use it extensively to render our scores right in our coding interface thanks to the interface provided by the Music21 Python package. The first line of code in each session should thus be !apt update, a command instructing Debian to fetch the list of packages installed in the Linux environment and those available for installation. Subsequently, !apt install musescore -y proceeds with the installation of Musescore, with the -y argument acting as a yes confirmation for the installation.

!apt update
!apt install musescore -y

Utilizing the command !pip install <package>, you can install any Python package of your choice, no separate installation confirmation needed with pip. For instance, if you’re keen on incorporating the Music21 package, as well as Djalgo, execute the following command.

!pip install music21 git+https://github.com/essicolo/djalgo.git
from music21 import environment
environment.set("musescoreDirectPNGPath", "/usr/bin/musescore")

Then you can start harmonizing with Python. Or maybe you’ll prefer installing everything on your machine?

Machine Setup Instructions

I recommend installing a Python release from the Python Foundation, available at python.org, version 3.11 or higher. However, other Python distributions like Anaconda and Miniforge will provide similar experience.

On Windows, once you have completed downloading the python-.exe file, simply double-click on the downloaded .exe file to start the installation process. Be sure to check the box that says “Add Python 3.x to PATH” at the bottom of the first installation screen to ensure that Python is added to your system’s environment variables. This action allows Python to be run from any command prompt.

On macOS, Python can be installed either from the .pkg file or using brew install python if you are a Homebrew user.

On Linux, Python should already be installed and accessible from the terminal.

To test if your installation was successful, open the Command Prompt on Windows (search for “cmd” in the Windows search bar) or a terminal on macOS and Linux, and then type python3. A command prompt with >>> should appear. Write 2+2, hit ↵, and, well… you know what to expect (pro-tip, not 5!). To exit Python, type quit().

To keep your workflow organized and reusable, you should create a virtual environment for each project you work on. The definition of a project, whether it encompasses everything related to music or pertains to a specific piece you are working on, is up to you. Personally, I create a virtual environment named “music” and compose from there. A virtual environment isolates a Python installation in a set of files, including a Python executable and the installed packages. You can delete a virtual environment by simply removing these files. If you need to recreate the virtual environment, all you need is the requirements.txt file, which lists all the necessary components to rebuild the environment. To create virtual environments, you must first install the Virtualenv package by running pip install virtualenv in a terminal (not in a Python command prompt). pip is the default package manager for Python.

Once Virtualenv is installed, create a new environment in your chosen folder. This could be a dedicated folder for all your environments, or a virtual-environment folder within your project directory. To navigate to this folder in a terminal, type cd path/to/location, where cd stands for change directory. Then, create your environment there with python3 -m venv music, where music is the name you assign to your environment. To activate the environment, type .\music\Scripts\activate on Windows, or on macOS or Linux, type source music/bin/activate. From there, install the necessary packages by typing pip install jupyterlab music21 git+https://github.com/essicolo/djalgo.git.

The Djalgo package contains the dependencies needed to follow the book. The JupyterLab package provides a very convenient way to interact with your code: litterate programming.

To tell Python where is the application to generate scores,

  • Windows: there are several ways to install MuseScore, explore to find the correct path if it doesn’t work.

from music21 import environment
environment.set("musescoreDirectPNGPath", "C:\\Program Files\\MuseScore 4\\bin\\MuseScore.exe")
  • macOS

from music21 import environment
environment.set("musescoreDirectPNGPath", "/Applications/MuseScore 4.app/Contents/MacOS/mscore")
  • Linux: the command will likely work if MuseScore is installed on the system, not from a flatpak, a snap or an appimage.

from music21 import environment
environment.set("musescoreDirectPNGPath", "/usr/bin/musecore")

Litterate programming

Literate programming emphasizes writing code between cells of text. This approach contrasts with the traditional code-centric perspective and aims to make code more understandable, and therefore, more reliable. Deepnote and Google Colab both adopt literate programing approaches, where each file is called a notebook. You can locally work on a similar interface with JupyterLab. Jupyter supports many programming languages, including Python.

JupyterLab offers a flexible and powerful environment for working with notebooks. Once installed, JupyterLab can be launched by simply typing jupyter lab in your terminal or command prompt, then the interface should pop up in your default browser. The interface provides an integrated environment where you can write code, view outputs, and add narrative text, images, tables and equations seamlessly. JupyterLab is designed to be extensible and modular, with features such as drag-and-drop cells, integrated data viewers, and tools for file management and version control. Additionally, a Jupyter-like interface can be loaded within the Visual Studio Code app by installing the Python extension.

Jupyter uses kernels to link the interface to the computing environment. Kernels are independant from Python virtual environments. A Python kernel can be installed from the Python virtual environment if the IPython and the IPykernel packages are installed. Open a terminal, activate your Python environment, then type ipython kernel install --user --name=<name> to create a new kernel named with a string that replaces <name>. On Linux, kernels should be stored in the folder .local/share/jupyter/kernels/ from you user directory. On macOS, they should stand in /Users/you/Library/Jupyter. On Windows, you should find them in %APPDATA%\jupyter\kernels\. To list you kernels, you can run jupyter kernelspec list. To delete them, you can either throw the folder in the bin or run jupyter kernelspec remove <name>. Users can select which kernel to use for each notebook via the kernel selection option in the notebook interface.

Python basics

Lists in First Class

We will cover some ways of working with music files in Python:

  • Music21 is dedicated to music theory and notation

  • Pretty-Midi is made to interact with MIDI files

Miditoolkit and SCAMP are not supported yet.

Although I will favor Music21 on this site to display our scores and export to MIDI, and Pretty-midi for more complex MIDI processes, but you can really choose the tool you prefer. This is because most of Djalgo’s outputs are generic Python objects like lists and tuples, which are simple and powerful ways to store information in Python. The content of a Python list is defined in square brackets, and each item is separated with a comma. In the next code cell, I assign a list to a variable.

[1]:
a = [1, 'a', 10, 'crocodile']
a
[1]:
[1, 'a', 10, 'crocodile']

Music as a signal of information

Djalgo is not about sound, but music. Sounds are very complex signals that can be generated from musical specifications such as notes. If you are more interested by shaping sounds, check out SCAMP, Sonic-Pi and SuperCollider, among many others. Rather than sounds, Djalgo generates numerical values representing notes. A note, at its most essential information, is a combinaision of a pitch, a duration, and when it starts in time. Another way of defining a note is its pitch, its start time and its end time. Since end time is start time plus duration, and duration is end time minus start time, both approaches contain the same information.

Djalgo numerically considers a note as a (pitch, duration, offset) tuple. Pitches are expressed in MIDI notation, a highly normed and complex way for music encoding, which spans from 0, corresponding to C2 (8.178 Hz), to 127, corresponding to G9 (12543.854 Hz). Durations, as well as offsets, or start times, are expressed in any unit desired, but, really, quarter lenghts should be used. A quarter length is the duration of a metronome tick. The metronome tick oscillates in beats per minute, a speed that allows quarter lengths to be placed in time.

In Python, tuples are immmutable lists: once it is defined, it can’t be altered. The tuple (72, 2.0, 1.0) defines a note with pitch C4 with a duration of two quarter lengths starting at 1.0 quarter length from thew begining of the track. Pitches defined by None are rests.

A rhythm is simply a note without the pitch. It is defined with a tuple of (duration, offset). Speaking of definitions, a track is a sequence of notes stored in a list. And multiple tracks form a piece, which becomes a list of lists.

Let’s define two tracks.

[2]:
twinkle_1 = [
    (60, 1.0, 0.0),  # C (twin)
    (60, 1.0, 1.0),  # C (kle)
    (67, 1.0, 2.0),  # G (twin)
    (67, 1.0, 3.0),  # G (kle)
    (69, 1.0, 4.0),  # A (lit)
    (69, 1.0, 5.0),  # A (tle)
    (67, 2.0, 6.0)  # G (star)
]

twinkle_2 = [
    (65, 1.0, 8.0),  # F (how)
    (65, 1.0, 9.0),  # F (I)
    (64, 1.0, 10.0), # E (won)
    (64, 1.0, 11.0), # E (der)
    (62, 1.0, 12.0), # D (what)
    (62, 1.0, 13.0), # D (you)
    (60, 2.0, 14.0)  # C (are)
]

To merge two lists horizontally, i.e. in the time direction, you can use the + opetator.

[3]:
twinkle = twinkle_1 + twinkle_2
twinkle
[3]:
[(60, 1.0, 0.0),
 (60, 1.0, 1.0),
 (67, 1.0, 2.0),
 (67, 1.0, 3.0),
 (69, 1.0, 4.0),
 (69, 1.0, 5.0),
 (67, 2.0, 6.0),
 (65, 1.0, 8.0),
 (65, 1.0, 9.0),
 (64, 1.0, 10.0),
 (64, 1.0, 11.0),
 (62, 1.0, 12.0),
 (62, 1.0, 13.0),
 (60, 2.0, 14.0)]

Stack them vertically creates a piece of two tracks.

[4]:
twinkle = [twinkle_1, twinkle_2]
twinkle
[4]:
[[(60, 1.0, 0.0),
  (60, 1.0, 1.0),
  (67, 1.0, 2.0),
  (67, 1.0, 3.0),
  (69, 1.0, 4.0),
  (69, 1.0, 5.0),
  (67, 2.0, 6.0)],
 [(65, 1.0, 8.0),
  (65, 1.0, 9.0),
  (64, 1.0, 10.0),
  (64, 1.0, 11.0),
  (62, 1.0, 12.0),
  (62, 1.0, 13.0),
  (60, 2.0, 14.0)]]

Leverage Djalgo for music composition

Scales

We haven’t used Djalgo yet. We just played with basic Python where I wrote the song Twinkle, Twinkle Little Star in C-major. C-major is a scale, i.e. a subset of the chromatic scale (all pitches) designed to fit together. Djalgo can generate pitch lists allowed for a given scale. We’ll need to load Djalgo in our session to access to its functionnalities. I use the alias dj to make the code shorter.

[5]:
import djalgo as dj

Scales are accessible from the harmony module. You have to define the tonic and the type, then .generate() will process the scale, returning all available MIDI pitches in the scale. The object.method() way of programming (object-oriented programming) is just like defining a frog and make it jump, as

frog = animal(order='anura')
frog.jump()
frog.swim()

In the following code block, I seek for the scale function in Djalgo, define it, then tell it to generate the scale.

[6]:
c_major = dj.harmony.Scale(tonic='C', mode='major').generate()
print(c_major)
[0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 17, 19, 21, 23, 24, 26, 28, 29, 31, 33, 35, 36, 38, 40, 41, 43, 45, 47, 48, 50, 52, 53, 55, 57, 59, 60, 62, 64, 65, 67, 69, 71, 72, 74, 76, 77, 79, 81, 83, 84, 86, 88, 89, 91, 93, 95, 96, 98, 100, 101, 103, 105, 107, 108, 110, 112, 113, 115, 117, 119, 120, 122, 124, 125, 127]

Scales are defined as intervals from the chromatic scale. You might have heard that a major scale is whole-step, whole-step, half-step, whole-step, whole-step, whole-step, half-step. In Python, from a list of 12 pitches in the chromatic scale, you would take the first pitch (index 0), the third (index 2), and so on. Djalgo predefines the major scale, the minor, diminished, pentatonic and so on.

[7]:
print(dj.harmony.Scale.scale_intervals)
{'major': [0, 2, 4, 5, 7, 9, 11], 'minor': [0, 2, 3, 5, 7, 8, 10], 'diminished': [0, 2, 3, 5, 6, 8, 9, 11], 'major pentatonic': [0, 2, 4, 7, 9], 'minor pentatonic': [0, 3, 5, 7, 10], 'chromatic': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 'lydian': [0, 2, 4, 6, 7, 9, 11], 'mixolydian': [0, 2, 4, 5, 7, 9, 10], 'dorian': [0, 2, 3, 5, 7, 9, 10], 'phrygian': [0, 1, 3, 5, 7, 8, 10], 'locrian': [0, 1, 3, 5, 6, 8, 10], 'harmonic minor': [0, 2, 3, 5, 7, 8, 11], 'melodic minor ascending': [0, 2, 3, 5, 7, 9, 11], 'melodic minor descending': [0, 2, 3, 5, 7, 8, 10]}

As any list, you can extract a subset by index. In Python, c_major[35:43] means you aim at extracting index 35 to excluding index 43, i.e. indexes 35 to 42. The resulting list is C4 to C5.

[8]:
c_major_sub = c_major[35:43] # C4 to C5
c_major_sub
[8]:
[60, 62, 64, 65, 67, 69, 71, 72]

To convert a list of pitches to the Djalgo notation, we could use afor loop. The explainations are in code comments, which are placed after the # sign.

[9]:
# Initialize an empty list to store the notes
c_major_sub_notes = []

# Initialize the offset, the first being 0
offset = 0

# Iterate over the pitches in the scale subset we assigned earlier
for pitch in c_major_sub:
    # Append the pitch, duration, and offset to the notes list
    c_major_sub_notes.append((pitch, 1, offset))
    # Increment the offset by 1
    offset = offset + 1

print(c_major_sub_notes)
[(60, 1, 0), (62, 1, 1), (64, 1, 2), (65, 1, 3), (67, 1, 4), (69, 1, 5), (71, 1, 6), (72, 1, 7)]

We now have a track, and can convert it to a Music21 object with the conversion utility, then render it with Music21’s .show() method.

[10]:
dj.conversion.convert(c_major_sub_notes, to='music21').show()
_images/02_harmony_23_0.png

Chords

A chord is multiple pitches played together, generally three. In Djalgo, chords are written as a list of pitches in the note format.

[11]:
c_major_chord = ([60, 64, 67], 1, 0)
dj.conversion.convert([c_major_chord], to='music21').show()
_images/02_harmony_25_0.png

The .show() method renders a score by default, but you have access to other rendering options, such as .show('midi').

Ornaments

We used the Djalgo package, but still haven’t seen how it can help to generate music. Let’s start with ornaments, which alter a list of notes to create a richer score. Djalgo has six types of ornaments: grace note, trill, mordent, arpeggio, turn and slide.

Grace note adds a note randomly drawned from the list given in grace_pitches at the place given by note_index.

[12]:
ornam = dj.harmony.Ornament(
    type='grace_note',
    grace_note_type='appoggiatura',
    grace_pitches=[72]
).generate(
    notes=c_major_sub_notes,
    note_index=4
)
dj.conversion.convert(ornam, to='music21').show()
_images/02_harmony_29_0.png

Trill gets the degree given by by from the note at note_index and oscillates at rate of trill_rate between the note and its degree.

[13]:
ornam = dj.harmony.Ornament(
    type='trill',
    trill_rate=0.125,
    by=1,
    tonic='C',
    mode='major'
).generate(
    notes=c_major_sub_notes,
    note_index=4
)
dj.conversion.convert(ornam, to='music21').show()
_images/02_harmony_31_0.png

Mordent rapidly alternates between the original pitch and one step defined by.

[14]:
ornam = dj.harmony.Ornament(
    type='mordent',
    by=-1,
    tonic='C',
    mode='major'
).generate(
    notes=c_major_sub_notes,
    note_index=4
)
dj.conversion.convert(ornam, to='music21').show()
_images/02_harmony_33_0.png

Arpeggio transforms a note to an arpeggio given by a list of degrees.

[15]:
ornam = dj.harmony.Ornament(
    type='arpeggio',
    tonic='C',
    mode='major',
    arpeggio_degrees=[0, 4, 2, 5]
).generate(
    notes=c_major_sub_notes,
    note_index=4
)
dj.conversion.convert(ornam, to='music21').show()
_images/02_harmony_35_0.png

Turn is a transition of four notes betweem note_index and the next note.

[16]:
ornam = dj.harmony.Ornament(
    type='turn',
    tonic='C',
    mode='major'
).generate(
    notes=c_major_sub_notes,
    note_index=4
)
dj.conversion.convert(ornam, to='music21').show()
_images/02_harmony_37_0.png

Silde is a glissando. However, in Djalgo, glissandos should be defined at the instrument level with your prefered package (Instrument in Pretty-midi and Stream in Music21). Instead of sliding, slide in djalgo transits on the chromatic scale from a note to the next.

[17]:
ornam = dj.harmony.Ornament(
    type='slide',
    slide_length=6
).generate(
    notes=[(60, 4, 0), (72, 4, 4)],
    note_index=0
)
dj.conversion.convert(ornam, to='music21').show()
_images/02_harmony_39_0.png

Voice

Voicing creates chords from pitch lists. These are just lists, but iterating through them can generate either chords and arpeggios.

[18]:
pitch_chords = dj.harmony.Voice(
    tonic = 'C',
    mode = 'major',
    degrees=[0, 2, 4] # triads
).generate(pitches=c_major_sub)
print(pitch_chords)
[[60, 64, 67], [62, 65, 69], [64, 67, 71], [65, 69, 72], [67, 71, 74], [69, 72, 76], [71, 74, 77], [72, 76, 79]]

The pitch_chords object contains only pitches. To create a track, we must also have durations and offsets. We could reuse the loop written earlier.

[19]:
c_major_sub_notes_chordified = []
offset = 0
for chord in pitch_chords:
    c_major_sub_notes_chordified.append((chord, 1, offset))
    offset = offset + 1
dj.conversion.convert(c_major_sub_notes_chordified, to='music21').show()
_images/02_harmony_43_0.png

Progression

Ever heard of the circle of fifths? It can be used to create progressions with chords that fit together. Why not using it to generate random progressions from different circles, the circle of fifths ('P5') being the most popular. The radius argument is the spread of the chords in the circle across [major chords, minor chords, diminished chords], usually [3, 3, 1].

[20]:
progression = dj.harmony.Progression(
    tonic_pitch='D3',
    circle_of='P5',
    radius=[3, 3, 1]
).generate(length=8, seed=5) # a seed is any random integer that allows you to reproduce the same outcomes from arandom process
progression_notes = []
offset = 0
for chord in progression:
    progression_notes.append((chord, 1, offset))
    offset = offset + 1
dj.conversion.convert(progression_notes, to='music21').show()
_images/02_harmony_45_0.png

Rhythms

For now, all note durations was set to 1 quarter length, and offsets were set accordingly. A combination of durations and offsets is called a rhythm in Djalgo. Rhythms can be set by hand, but to leverage Djalgo, we can generate them randomly. The random method from the rhythm module draws numbers from a durations list until they sum up to the measure_length.

[21]:
random_rhythm = dj.rhythm.Rhythm(
    measure_length=8,
    durations = [0.5, 1, 2]
).random(seed=3)
random_rhythm
[21]:
[(0.5, 0.0),
 (0.5, 0.5),
 (2, 1.0),
 (2, 3.0),
 (0.5, 5.0),
 (1, 5.5),
 (0.5, 6.5),
 (1, 7.0)]

A random progression of the same length can be generated (len(a) take the length of the list a), mapped to the rhythm, and transformed to a Music21 stream to create a score or a midi.

[22]:
progression = dj.harmony.Progression(
    tonic_pitch='C3',
    circle_of='P5',
    radius=[3, 3, 1]
).generate(length=len(random_rhythm), seed=5)
random_progression = [(p, d, o) for p, (d, o) in zip(progression, random_rhythm)]
dj.conversion.to_music21(random_progression).show()
_images/02_harmony_49_0.png

Wrap up

Let’s take our twinkle song.

[23]:
twinkle = twinkle_1 + twinkle_2
dj.conversion.to_music21(twinkle).show()
_images/02_harmony_51_0.png

We will add a voice to index 0 and index 7.

[24]:
twinkle_pitches = [note[0] for note in twinkle]
twinkle_chords = dj.harmony.Voice(
    tonic = 'C',
    mode = 'major',
    degrees=[0, 2, 4] # triads
).generate(pitches=twinkle_pitches)
for i in [0, 7]:
    twinkle[i] = (twinkle_chords[i], twinkle[i][1], twinkle[i][2])
dj.conversion.to_music21(twinkle).show()
_images/02_harmony_53_0.png

Some ornaments…

[25]:
twinkle = dj.harmony.Ornament(
    type='trill',
    trill_rate=0.25,
    by=1,
    tonic='C',
    mode='major'
).generate(
    notes=twinkle,
    note_index=6
)

twinkle = dj.harmony.Ornament(
    type='mordent',
    trill_rate=0.25,
    by=1,
    tonic='C',
    mode='major'
).generate(
    notes=twinkle,
    note_index=len(twinkle) - 1
)

dj.conversion.convert(twinkle, to='music21').show()
_images/02_harmony_55_0.png

To avoid an abrupt ending, let’s alter the last duration to 4.667, so that the last note of the mordent ends up its measure and lasts another measure.

[26]:
twinkle[-1] = (twinkle[-1][0], 4.667, twinkle[-1][2])

And let’s hear our masterpiece!

[27]:
dj.conversion.convert(twinkle, to='music21').show('midi')

Another way to hear it is using the (wonderful) SCAMP library.

[28]:
import scamp # make sure its installed with pip install scamp
s = scamp.Session(tempo=120)
instrument = s.new_part('flute')
for i,n in enumerate(twinkle):
    # play either a note or a chord
    if isinstance(n[0], list):
        instrument.play_chord(n[0], 1, n[1])
    else:
        instrument.play_note(n[0], 1, n[1])
    # the following is not necessary here, it just assures that rests are respected
    #if i < (len(twinkle)-1):
    #    scamp.wait(twinkle[i+1][2]-(n[1] + n[2]))
s.wait_for_children_to_finish() # end the SCAMP session in notebooks
Using preset Flute Gold for flute

Exporting your music to midi depends on the object you have converted your music to.

[29]:
dj.conversion.convert(twinkle, to='music21').write('midi', '_midi-output/twinkle_m21.mid') # with music21
dj.conversion.convert(twinkle, to='pretty_midi').write('twinkle_pm.mid') # with pretty-midi

You should now be able to modify a piece, decorate it, and export it. Next step:

Loops