An introduction to durations¶
In this demonstration we reconstruct the first part of the Kyrie in Ockeghem’s Missa Prolationum.
Ockeghem, Missa Prolationum¶
The Kyrie is structured as a double canon, where the imitation replies with an augmented version by interpreting the durations using a different prolatio. In some cases there are minor exceptions. To account for these we use properties to set the augmented duration of the imitation (see the notes with ‘setdur’).
Properties: Any note/chord can have a user-defined set of properties. These properties are implemented as a dict mapping string keys to any value and are normally used to attach data to a note in place in order to perform some action later
[1]:
from maelzel.core import *
Cantus¶
The first voice of the canon, the cantus.
[2]:
cantus = Chain([
Note("4F", 4),
Note("4C", 4),
Note("4F", 4),
Note("4A", 4),
Rest(2),
# Each Note/Chord can have user-defined properties. These can be used to attach any data to them
Note("4E", 2, properties={'setdur': 4}),
Note("4F", 4),
Note("4E", 2),
Note("4C", 2),
Note("4C", 2),
Rest(2),
Note("4F", 4, properties={'setdur': 4}),
Note("4G", 3),
Note("4E", 1),
Note("4E", 2),
Note("4F", 3),
Note("4A", 1.5),
Note("4B", 0.5),
Note("5C", 1),
Note("4B", 1),
Note("4B", 2),
Note("4A", 0.5),
Note("4B", 0.5),
Note("5C", 2),
Rest(2),
Note("4A", 2),
Note("4F", 2),
Note("4G", 2),
Note("4F", 2),
Note("4E", 2),
Note("4C", 4, properties={'setdur': 4}),
Note("5C", 2),
Note("4Bb", 1),
Note("5D", 2),
Note("5C", 0.5),
Note("4Bb", 0.5),
Note("4A", 6),
Note("4F", 2),
Note("4F", 3),
Note("4E", 0.5),
Note("4D", 0.5),
Note("4C", 2)
])
Contra¶
[3]:
contra = Chain([
Note("4F", 6),
Note("4A", 3),
Note("4F", 3, properties={'setdur': 6}),
Note("5C", 6),
Note("4A", 3),
Note("4F", 3, properties={'setdur': 6}),
Note("4C", 4),
Note("5C", 2),
Note("5C", 2),
Note("4A", 2),
Note("4A", 2),
Note("4G", 4),
Note("4A", 2),
Note("4F", 2),
Note("5C", 4),
Note("5D", 2),
Note("5D", 2),
Note("5C", 4),
Note("4F", 2),
Note("4A", 2),
Note("4G", 1.5),
Note("4F", 0.5),
Note("4D", 2),
Note("4A", 3),
Note("4B", 1),
Note("5C", 2),
Note("4A", 2),
Rest(1),
Note("4Bb", 2),
Note("4A", 1),
Note("5C", 2),
Note("4F", 4),
Note("4F", 2),
Note("3Bb", 4),
Note("4F", 2),
Note("4F", 1),
Note("4A", 2),
Note("4B", 1),
Note("5C", 2)
])
contra = contra.transpose(-12)
Here we calculate the duration of the imitation, using the previously set properties to account for the exceptions to the rule
[4]:
def cantusProlatio(n):
if dur := n.getProperty('setdur'):
return dur
elif n.dur == 4:
return n.dur * 1.5
return n.dur
cantus2 = Chain([n.clone(dur=cantusProlatio(n)) for n in cantus])
Score([cantus, cantus2])
[4]:
Score(2 voices)
[5]:
def contraProlatio(n):
if dur := n.getProperty('setdur'):
return dur
elif n.dur == 6:
return n.dur * F(3, 2)
return n.dur
contra2 = Chain(n.clone(dur=contraProlatio(n))
for n in contra)
Score([contra, contra2])
[5]:
Score(2 voices)
[8]:
struct = ScoreStruct(timesig=(2, 2), tempo=120)
# setScoreStruct(struct)
origscore = Score([Voice(cantus, name='cantus'),
Voice(cantus2, name='cantus2'),
Voice(contra, name='contra'),
Voice(contra2, name='contra2')
], scorestruct=struct)
origscore
[8]:
Score(4 voices)
For playback we will use a very simple singing voice based on the formants of a male tenor. To match the range it might be a good idea to transpose the score down.
[9]:
s = origscore.transpose(-5)
s
[9]:
Score(4 voices)
Playback¶
For playback we can define a simple singing preset with formant values for a male voice. Here we will not get into the details: for now it is enough to notice that we define values for 5 vowels (a, e, i, o, u) which are placed on the 2D plane and we can select a vowel by setting its corresponding coordinate
[10]:
defPreset('sing',
init=r"""
gi__formantFreqs__[] fillarray \
668, 1191, 2428, 3321, 4600, \ ; A
327, 2157, 2754, 3630, 4600, \ ; E
208, 2152, 3128, 3425, 4200, \ ; I
335, 628, 2689, 3515, 4200, \ ; O
254, 796, 2515, 3274, 4160 ; U
gi__formantDbs__[] fillarray \
28, 28, 22, 20, 20, \
15, 25, 24, 20, 23, \
10, 20, 27, 26, 20, \
15, 18, 5, 7, 12, \
12, 10, 6, 5, 12
gi__formantBws__[] fillarray \
80, 90, 120, 130, 140, \
60, 100, 120, 150, 200, \
60, 90, 100, 120, 120, \
40, 80, 100, 120, 120, \
50, 60, 170, 180, 200
gi__formantAmps__[] maparray gi__formantDbs__, "ampdb"
reshapearray gi__formantFreqs__, 5, 5
reshapearray gi__formantAmps__, 5, 5
reshapearray gi__formantBws__, 5, 5
""",
audiogen = r"""
|kx=0, ky=0, kvibamount=1|
kx *= random:i(0.95, 1.05)
ky *= randomi:k(0.96, 1.04, 1)
kvibfreq = linsegr:k(0, 0.1, 0, 0.4, 4.7, 0.2, 1) * randomi:k(0.9, 1.1, 3)
kvibsemi = linsegr:k(0, 0.2, 0, 0.8, 0.4, 0.2, 0) * randomi:k(0.85, 1.15, 8)
kvib = oscil:k(kvibsemi/2, kvibfreq) - kvibsemi/2
kpitch = lag:k(kpitch, 0.2) + kvib*kvibamount
asource = butterlp:a(vco2:a(kamp, mtof(kpitch)), 4000)
; x y weight
kcoords[] fillarray 0, 0, 1, \ ; A
0.5, 0.5, 0.3, \ ; E
1, 0, 1, \ ; I
0, 1, 1, \ ; O
1, 1, 1 ; U
kweights[] presetinterp kx, ky, kcoords, 0.2
kformantFreqs[] weightedsum gi__formantFreqs__, kweights
kformantBws[] weightedsum gi__formantBws__, kweights
kformantAmps[] weightedsum gi__formantAmps__, kweights
kformantFreqs poly 5, "lag", kformantFreqs, 0.2
kformantAmps poly 5, "lag", kformantAmps, 0.2
aformants[] poly 5, "resonx", asource, kformantFreqs, kformantBws, 2, 1
aformants *= kformantAmps
aout1 = sumarray(aformants) * 0.1
"""
)
[10]:
(routing=True, numouts=2, numsignals=1)
init: gi__formantFreqs__[] fillarray \
668, 1191, 2428, 3321, 4600, \ ; A
327, 2157, 2754, 3630, 4600, \ ; E
208, 2152, 3128, 3425, 4200, \ ; I
335, 628, 2689, 3515, 4200, \ ; O
254, 796, 2515, 3274, 4160 ; U
gi__formantDbs__[] fillarray \
28, 28, 22, 20, 20, \
15, 25, 24, 20, 23, \
10, 20, 27, 26, 20, \
15, 18, 5, 7, 12, \
12, 10, 6, 5, 12
gi__formantBws__[] fillarray \
80, 90, 120, 130, 140, \
60, 100, 120, 150, 200, \
60, 90, 100, 120, 120, \
40, 80, 100, 120, 120, \
50, 60, 170, 180, 200
gi__formantAmps__[] maparray gi__formantDbs__, "ampdb"
reshapearray gi__formantFreqs__, 5, 5
reshapearray gi__formantAmps__, 5, 5
reshapearray gi__formantBws__, 5, 5
|kx=0.0, ky=0.0, kvibamount=1.0|
kx *= random:i(0.95, 1.05)
ky *= randomi:k(0.96, 1.04, 1)
kvibfreq = linsegr:k(0, 0.1, 0, 0.4, 4.7, 0.2, 1) * randomi:k(0.9, 1.1, 3)
kvibsemi = linsegr:k(0, 0.2, 0, 0.8, 0.4, 0.2, 0) * randomi:k(0.85, 1.15, 8)
kvib = oscil:k(kvibsemi/2, kvibfreq) - kvibsemi/2
kpitch = lag:k(kpitch, 0.2) + kvib*kvibamount
asource = butterlp:a(vco2:a(kamp, mtof(kpitch)), 4000)
; x y weight
kcoords[] fillarray 0, 0, 1, \ ; A
0.5, 0.5, 0.3, \ ; E
1, 0, 1, \ ; I
0, 1, 1, \ ; O
1, 1, 1 ; U
kweights[] presetinterp kx, ky, kcoords, 0.2
kformantFreqs[] weightedsum gi__formantFreqs__, kweights
kformantBws[] weightedsum gi__formantBws__, kweights
kformantAmps[] weightedsum gi__formantAmps__, kweights
kformantFreqs poly 5, "lag", kformantFreqs, 0.2
kformantAmps poly 5, "lag", kformantAmps, 0.2
aformants[] poly 5, "resonx", asource, kformantFreqs, kformantBws, 2, 1
aformants *= kformantAmps
aout1 = sumarray(aformants) * 0.1
Make the voices sing some “random” vowels
[16]:
vowels= {
'a': (0, 0, 1),
'e': (0.5, 0.5, 1),
'i': (1, 0, 1.3),
'o': (0, 1, 2),
'u': (1, 1, 2)
}
# Vowel patterns for each voice
patterns = ["aaeo", "auoai", "aeaoe", "aiuoeu"]
# Panning position for each voice
positions = [0, 0.3, 0.7, 1]
for voice, pattern, position in zip(s.voices, patterns, positions):
voice.setPlay(instr='sing', position=position)
for i, n in enumerate(voice):
if n.isRest():
continue
vowel = pattern[i%len(pattern)]
x, y, gain = vowels[vowel]
gain *= 1 if n.dur >= 1 else 1.3
# Set the vowel coords in advance
n.setPlay(args={'kx': x, 'ky': y}, gain=gain)
We can add a very simple reverb to the playback. For that we access the underlying sound session, which is a csoundengine’s Session
(https://csoundengine.readthedocs.io/en/latest/session.html)
[13]:
session = playSession()
session.defInstr("postproc", r'''
a1, a2 monitor
a1post, a2post reverbsc a1, a2, 0.75, 12000, sr, 0.5, 1
outch 1, a1post - a1, 2, a2post - a2
''')
[13]:
a1, a2 monitor
a1post, a2post reverbsc a1, a2, 0.75, 12000, sr, 0.5, 1
outch 1, a1post - a1, 2, a2post - a2
We render the result to a soundfile for the purpose of having the audio embedded in the notebook. We could just as well render it in realtime by replacing render
with play
[17]:
# Make sure that the reverb has time to die off
totalDuration = s.durSecs() + 4
with render() as r:
r.sched('postproc', dur=totalDuration, priority=2)
s.play(sustain=0.15, fade=(0.05, 0.15))
r
[17]:
"/home/em/.local/share/maelzel/recordings/rec-2023-03-27T20:37:57.358.wav"
, 2
channels, 58.00
secs, 44100
Hz)[ ]: