python-course.eu

17. Transforming Fibonacci Numbers into Music.

By Bernd Klein. Last modified: 01 Feb 2022.

Fibonacci Sequence

The Fibonacci sequence or numbers - named after Leonardo of Pisa, also known as Fibonacci - have fascinated not only mathematicians for centuries. Fibonacci formulated an exercise about the rabbits and their reproduction: In the beginning there is one pair of rabbits (male and female), born out of the blue sky. It takes one month until they can mate. At the end of the second month the female gives birth to a new pair of rabbits. It works the same way with every newly born pair of rabbits, i.e. that it takes one month until the female one can mate and after another month she gives birth to a new pair of rabbits. Now let's suppose that every female rabbit will bring forth another pair of rabbits every month after the end of the first month. Be aware of the fact that these "mathematical" rabbits are immortal. So the population for the the generations look like this:

1, 1, 2, 3, 5, 8, 13, 21, ...

We can easily see that each new number is the sum of the previous two.

We get to music very soon, and feel free to skip the mathematics, but one thing is also worth mentioning. The Fibonacci numbers are strongly related to the Golden Ratio $\varphi$:

$$\varphi = {\frac {1 + \sqrt{5}} {2}}$$

because the quotient of last and the previous to last number in this seqence is getting closer and closer to $\varphi$:

$$\lim_{n\to\infty} { F_n \over F_{n-1}} = \varphi$$

($F_n stands for the n-th Fibonacci number)

Fibonacci Score

The Fibonacci numbers, often presented in conjunction with the golden ratio, are a popular theme in culture. The Fibonacci numbers have been used in the visual art and architecture. The have been used in music very often. My favorite example is the song Lateralus by Tool. The text is rhythmically grouped in Fibonacci numbers. If you look at the following lines , you can count the syllables and you will get 1, 1, 2, 3, 5, 8, 5, 3, 13, 8, 5, 3:

Black 
then 
white are
all I see
in my infancy
Red and yellow then came to be, 
reaching out to me
Lets me see

As below, so above and beyond, I imagine
Drawn beyond the lines of reason
Push the envelope, 
watch it bend

We will create a piano score for Fibonacci numbers in this chapter. There is no unique way to do this. We will create both a PDF score our "composition" and a midi file, so that you can listen to the result. We will use LilyPond to create the score:

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Enrol here

LilyPond

What is LilyPond? On the websitelypond.org they write the following:

LilyPond is a music engraving program, devoted to producing the highest-quality sheet music possible. It brings the aesthetics of traditionally engraved music to computer printouts. LilyPond is free software and part of the GNU Project.

You can learn more about Lilypond in the chapter Musical Scores with Python of our Python tutorial. The following page give you also a great impression how LilyPond works: 'Compiling' Music

%%writefile simple_example.ly
\version "2.14.1"
\include "english.ly"

\score {
  \new Staff {
    \key d \major
    \numericTimeSignature
    \time 2/4
    <cs' d'' b''>16 <cs' d'' b''>8.
    %% Here: the tie on the D's looks funny
    %% Too tall? Left-hand endpoint is not aligned with the B tie?
    ~
    <cs' d'' b''>8 [ <b d'' a''> ]
  }
}

OUTPUT:

Overwriting simple_example.ly
!lilypond  simple_example.ly

OUTPUT:

GNU LilyPond 2.20.0
Processing `simple_example.ly'
Parsing...
Interpreting music...
Preprocessing graphical objects...
Finding the ideal number of pages...
Fitting music on 1 page...
Drawing systems...
Layout output to `/tmp/lilypond-R5QDyE'...
Converting to `simple_example.pdf'...
Deleting `/tmp/lilypond-R5QDyE'...
Success: compilation successfully completed

You can see the result by looking at the pdf file simple_example.pdf. The original file from which the PDF was created is simple_example.ly

Fibonacci Score

The Fibonacci Function

To create our Fibonacci score we will use the following Fibonacci function. You can find further explanation - most probably not necessary for the understanding of this chapter - concerning the Fibonacci function in our chapter Recursive Functions and additionally in our chapters Memoization with Decorators and Generators and Iterators.

class FibonacciLike:

    def __init__(self, i1=0, i2=1):
        self.memo = {0:i1, 1:i2}

    def __call__(self, n):
        if n not in self.memo: 
            self.memo[n] = self.__call__(n-1) + self.__call__(n-2)  
        return self.memo[n]
    
fib = FibonacciLike()

Furthmore, we will use the function gcd, which calculates the greatest common divisor of two positive numbers:

def gcd(a, b):
    """ returns the greatest common divisor of the
    numbers a and b """
    while b != 0:
        a, b = b, a%b
    return a

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Enrol here

Numbers to Notes

We map the numbers 0 to 10 to the notes of the E major scale:

%%writefile digits_to_notes.ly
<<
  \relative c' {
    \key g \major
    \time 6/8
    dis2 e2 fis2 gis2 a2 b2 cis2 dis2 e2 fis2
  }
  \addlyrics {
    "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "10"
  }
>>

OUTPUT:

Overwriting digits_to_notes.ly
!lilypond  digits_to_notes.ly

OUTPUT:

GNU LilyPond 2.20.0
Processing `digits_to_notes.ly'
Parsing...
digits_to_notes.ly:1: warning: no \version statement found, please add

\version "2.20.0"

for future compatibility
Interpreting music...
Preprocessing graphical objects...
Finding the ideal number of pages...
Fitting music on 1 page...
Drawing systems...
Layout output to `/tmp/lilypond-KrL4cS'...
Converting to `digits_to_notes.pdf'...
Deleting `/tmp/lilypond-KrL4cS'...
Success: compilation successfully completed

We need a mapping of digits to notes. We will use an e major scale and give each note the following numbers:

Notes

In Python we do this mapping by using a list and the indices correspond to the numbers abo

digits2notes = ["dis", "e", "fis", 
                "gis", "a", "b", 
                "cis'", "dis'", "e'", 
                "fis'"]
#digits2notes = ["b", "c", "d", "e", "f", "g", "a'", "b'", "c'", "d'"]
notes = str(fib(24)) + str(fib(12))
notes = str(fib(24)) 
notes

OUTPUT:

'46368'
def notesgenerator(func, *numbers,k=1):
    """
    notesgenerator takes a function 'func', which will be applied to
    the tuple of 'numbers'. The results are transformed into strings 
    and concatenated into a string 'notes'
    The length of the string 'notes' is the first value which will be yielded
    by a generator object.
    After this it will cycle forever over the notes and will each
    time return the next k digits. The value of k can be changed be
    calling the iterator with send and a new k-value.
    
    
    Example:
    notesgenerator(fib, 1, 1, 2, 3, 5, 8, k=2)
    The results of applying fib to the numbers are
    1, 1, 1, 2, 5, 21 
    The string concatenations gives us 
    1112521
    When called the first time, it will yield 7, i.e. the length of
    the string notes.
    After this it will yield 11, 12, 52, 11, 11, 25, 21, 11, 12, ...
    """
    func_values_as_strings = [str(func(x)) for x in numbers]
    notes = "".join( func_values_as_strings)
    pos = 0
    n = len(notes)
    rval = yield n
    while True:
        new_pos = (pos + k) % n
        if pos + k < n:
            rval = yield notes[pos:new_pos]
        else:
            rval = yield notes[pos:] + notes[:new_pos] 
        if rval:
            k = rval
        pos = new_pos
        
from itertools import cycle
note = notesgenerator(fib, 5, 8, 13, k=1)

print("length of the created 'notes' string", next(note))
for i in range(5):
    print(next(note), end=", ")
print("We set k to 2:")
print(note.send(2))
for i in range(5):
    print(next(note), end=", ")

OUTPUT:

length of the created 'notes' string 6
5, 2, 1, 2, 3, We set k to 2:
35
21, 23, 35, 21, 23, 
def create_variable_score(notes_gen,
                          upper, 
                          lower, 
                          beat_numbers,
                          number_of_notes=80):
    """ create_variable_score creates the variable part of our score, 
    i.e. without the header. It consists of a melody with a chords accompaniment.
    """
    
    counter = 0
    old_res = ""
    for beat in cycle(beat_numbers):
        res = notes_gen.send(beat)
        #print(res)
        seq = ""
        if beat == 1:
            upper += digits2notes[int(res[0])] + "4 " 
        elif beat == 2:
            for i in range(beat):
                upper += digits2notes[int(res[i])] + "8 "
        elif beat == 3:
            upper += " \\times 2/3 { " 
            note = digits2notes[int(res[0])]
            upper += note + "8 "
            for i in range(1, beat):
                note = digits2notes[int(res[i])]
                upper += note + " "
            upper += "}"
        #elif beat == 4:
        #    for i in range(beat):
        #        upper += digits2notes[int(res[i])] + "16 "
        elif beat == 5:
            upper += " \\times 4/5 { " 
            note = digits2notes[int(res[0])]
            upper += note + "16 "
            for i in range(1, beat):
                note = digits2notes[int(res[i])]
                upper += note + " "
            upper += "}"
        elif beat == 8:
            upper += " \\times 8/8 { " 
            note = digits2notes[int(res[0])]
            upper += note + "32 "
            for i in range(1, beat):
                note = digits2notes[int(res[i])]
                upper += note + " "
            upper += "}"
        if counter > number_of_notes:
            break

        if old_res:
            accord = set()
            for i in range(len(old_res)):
                note = digits2notes[int(old_res[i])]
                accord.add(note)
            accord = " ".join(list(accord))
            lower += "<" +  accord + ">"

        else:
            lower += "r4 "

        counter += 1

        old_res = res
    
    return upper + "}", lower +  "}"
def create_score_header(key="e \major", time="5/4"):
    
    """ This creates the score header """
    
    upper = r"""
\version "2.20.0"
upper = \fixed c' {
  \clef treble
  \key """ + key  +      r"""
  \time """ + time + r"""
"""

    lower = r"""
lower = \fixed c {
  \clef bass
  \key """ + key  +      r"""
  \time """ + time + r"""

"""
    return upper, lower
def create_score(upper, 
                 lower, 
                 number_seq):
    """ create score combines everything to the final score """
    
    title = ",".join([str(x) for x in number_seq])
    score = upper + "\n" + lower + "\n" + r"""
    \header {
      title = " """ 

    score += title 
    score += r""" Fibonacci"
      composer = "Bernd Klein"
    }

    \score {
      \new PianoStaff \with { instrumentName = "Piano" }
      <<
        \new Staff = "upper" \upper
        \new Staff = "lower" \lower
      >>
      \layout { }
      \midi {\tempo 4 = 45 }
    }
    """
    return score
from itertools import cycle
number_seq = (2021,)
x = notesgenerator(fib, *number_seq)

print(next(x))

upper, lower = create_score_header()

t = (1, 1, 2, 3, 5, 5, 3, 2, 1, 1, 2, 3, 5, 8, 8, 5, 3, 2, 1, 1)
# = (1, 1, 2, 3, 5, 8)
ng = notesgenerator(fib, *number_seq)
number_of_notes = next(ng)
print(f"n of notes: {number_of_notes}")
upper, lower = create_variable_score(ng,
                                     upper, 
                                     lower,
                                     t,
                                    number_of_notes)

score = create_score(upper, lower, number_seq)

open("score.ly", "w").write(score)

OUTPUT:

423
n of notes: 423
13555

With the previous code we created the file score.ly which is the lilypond reüresentation of our score. To create a PDF version and a midi file, we have to call lilypond on a command line with

lilypond score.ly

The following call with !lilypond score.ly is the equivalent in a jupter notebook cell:

!lilypond  score.ly

OUTPUT:

GNU LilyPond 2.20.0
Processing `score.ly'
Parsing...
Interpreting music...[8][16][24][32][40][48][56][64][72][80]
Preprocessing graphical objects...
Interpreting music...
MIDI output to `score.midi'...
Finding the ideal number of pages...
Fitting music on 6 or 7 pages...
Drawing systems...
Layout output to `/tmp/lilypond-TcDnC5'...
Converting to `score.pdf'...
Deleting `/tmp/lilypond-TcDnC5'...
Success: compilation successfully completed

In case you cannot create create it yourself, you can downoad it here:

We created the mp3 file with the following Linux commands:

timidity score.midi -Ow -o - | ffmpeg -i - -acodec libmp3lame -ab 64k score.mp3

The program timidity can also be used to listen to the midi file by calling it on the command line:

timidity score.midi

Polyphonic Score from Fibonacci Numbers

While the first score consists of a melody underlaid with chords, we now want to create a score that has a melody both in the left and right hand.

def create_polyphone_score(notes_gen,
                           upper, 
                           lower, 
                           beat_numbers,
                           number_of_notes=80):
    """ This function is similar to create_variable score 
    but it creates melodies in the left and right hand."""
    
    counter = 0
    old_res = ""
    group = ""
    print(beat_numbers)
    for beat in cycle(beat_numbers):
        res = notes_gen.send(beat)
        #print(beat, res)
        seq = ""
        old_group = group
        if beat == 1:
            group = digits2notes[int(res[0])] + "4 " 
            upper += group
        elif beat == 2:
            group = ""
            for i in range(beat):
                group += digits2notes[int(res[i])] + "8 "
            upper += group
        elif beat == 3:
            group = ""
            group += "\\times 2/3 { " 
            note = digits2notes[int(res[0])]
            group += note + "8 "
            for i in range(1, beat):
                note = digits2notes[int(res[i])]
                group += note + " "
            group += "}"
            upper += group
        #elif beat == 4:
        #    for i in range(beat):
        #        upper += digits2notes[int(res[i])] + "16 "
        elif beat == 5:
            group = ""
            group += "\\times 4/5 { " 
            note = digits2notes[int(res[0])]
            group += note + "16 "
            for i in range(1, beat):
                note = digits2notes[int(res[i])]
                group += note + " "
            group += "}"
            upper += group
        elif beat == 8:
            group = ""
            group += "{ " 
            note = digits2notes[int(res[0])]
            group += note + "32 "
            for i in range(1, beat):
                note = digits2notes[int(res[i])]
                group += note + " "
            group += "}"
            upper += group
        elif beat == 13:
            group = ""
            group += "\\times 4/13 { " 
            note = digits2notes[int(res[0])]
            group += note + "16 "
            for i in range(1, beat):
                note = digits2notes[int(res[i])]
                group += note + " "
            group += "}"
            upper += group


        if old_group:
            lower += old_group      
        else:
            lower += "r4 "

        counter += beat
        if counter > number_of_notes:
            break

        old_res = res
    return upper +  "}", lower +  "}"
from itertools import cycle

upper, lower = create_score_header()

t = (1, 1, 2, 3, 5, 5, 3, 2, 1, 1, 2, 3, 5, 8, 8, 5, 3, 2, 1, 1)
t = (1, 1, 2, 3, 5, 8, 13, 5, 1, 1)
ng = notesgenerator(fib, *number_seq)
number_of_notes = next(ng)
print(f"n of notes: {number_of_notes}")
upper, lower = create_polyphone_score(ng,
                                      upper, 
                                      lower,
                                      t,
                                      number_of_notes*3)


score = create_score(upper, lower, number_seq)

open("score2.ly", "w").write(score)

OUTPUT:

n of notes: 423
(1, 1, 2, 3, 5, 8, 13, 5, 1, 1)
14497
!lilypond  score2.ly

OUTPUT:

GNU LilyPond 2.20.0
Processing `score2.ly'
Parsing...
Interpreting music...[8][16][24][32][40][48][56][64][64]
Preprocessing graphical objects...
Interpreting music...
MIDI output to `score2.midi'...
Finding the ideal number of pages...
Fitting music on 4 or 5 pages...
Drawing systems...
Layout output to `/tmp/lilypond-jakQyo'...
Converting to `score2.pdf'...
Deleting `/tmp/lilypond-jakQyo'...
Success: compilation successfully completed

You can download the previously created files:

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Enrol here

Pentatonic Score from Fibonacci Numbers

In the following example, we will use a Pentatonic scale:

digits2notes = ["a,", "c", "d", 
                "e", "g", "a", 
                "c'", "d'", "e'", 
                "g'"]
from itertools import cycle



upper, lower = create_score_header(key="a \minor", time="4/4")


t = (1, 1, 2, 3, 5, 5, 3, 2, 1, 1, 2, 3, 5, 8, 8, 5, 3, 2, 1, 1)
t = (1, 1, 2, 3, 5, 8, 13, 5, 1, 1)
ng = notesgenerator(fib, *number_seq)
number_of_notes = next(ng)

print(f"n of notes: {number_of_notes}")
upper, lower = create_polyphone_score(ng,
                                      upper, 
                                      lower,
                                      t,
                                      number_of_notes*3)


score = create_score(upper, lower, number_seq)

open("score3.ly", "w").write(score)

OUTPUT:

n of notes: 423
(1, 1, 2, 3, 5, 8, 13, 5, 1, 1)
11591
!lilypond  score3.ly

OUTPUT:

GNU LilyPond 2.20.0
Processing `score3.ly'
Parsing...
Interpreting music...[8][16][24][32][40][48][56][64][72][80][80]
Preprocessing graphical objects...
Interpreting music...
MIDI output to `score3.midi'...
Finding the ideal number of pages...
Fitting music on 7 or 8 pages...
Drawing systems...
Layout output to `/tmp/lilypond-Lj1dnx'...
Converting to `score3.pdf'...
Deleting `/tmp/lilypond-Lj1dnx'...
Success: compilation successfully completed

You can download the previously created files:

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See: Live Python courses overview

Enrol here