{ "cells": [ { "cell_type": "markdown", "id": "6f294ce6-2c68-4a4b-a65a-18e932552683", "metadata": {}, "source": [ "# An introduction to durations\n", "\n", "In this demonstration we reconstruct the first part of the Kyrie in Ockeghem's Missa Prolationum. " ] }, { "cell_type": "markdown", "id": "74b208a1-2779-4f26-bc8d-7fb15fba76ac", "metadata": {}, "source": [ "## Ockeghem, Missa Prolationum\n", "\n", "\"Score\"" ] }, { "cell_type": "markdown", "id": "8a2f2de3-aaad-4686-907f-f50734ed0394", "metadata": {}, "source": [ "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'). \n", "\n", "> **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" ] }, { "cell_type": "code", "execution_count": 1, "id": "b0588d45-c031-493c-89cd-c78fc6e5dec2", "metadata": {}, "outputs": [], "source": [ "from maelzel.core import *" ] }, { "cell_type": "markdown", "id": "f8a1da4d-4d93-47a2-81dc-2a85f6d38829", "metadata": {}, "source": [ "### Cantus\n", "\n", "The first voice of the canon, the cantus. " ] }, { "cell_type": "code", "execution_count": 2, "id": "75725fa6-3c0b-4dc6-85f9-4f4404ae861f", "metadata": {}, "outputs": [], "source": [ "cantus = Chain([\n", " Note(\"4F\", 4),\n", " Note(\"4C\", 4),\n", " Note(\"4F\", 4),\n", " Note(\"4A\", 4),\n", " Rest(2),\n", " # Each Note/Chord can have user-defined properties. These can be used to attach any data to them\n", " Note(\"4E\", 2, properties={'setdur': 4}),\n", " Note(\"4F\", 4),\n", " Note(\"4E\", 2),\n", " Note(\"4C\", 2),\n", " Note(\"4C\", 2),\n", " Rest(2),\n", " Note(\"4F\", 4, properties={'setdur': 4}),\n", " Note(\"4G\", 3),\n", " Note(\"4E\", 1),\n", " Note(\"4E\", 2),\n", " Note(\"4F\", 3),\n", " Note(\"4A\", 1.5),\n", " Note(\"4B\", 0.5),\n", " Note(\"5C\", 1),\n", " Note(\"4B\", 1),\n", " Note(\"4B\", 2),\n", " Note(\"4A\", 0.5),\n", " Note(\"4B\", 0.5),\n", " Note(\"5C\", 2),\n", " Rest(2),\n", " Note(\"4A\", 2),\n", " Note(\"4F\", 2),\n", " Note(\"4G\", 2),\n", " Note(\"4F\", 2),\n", " Note(\"4E\", 2),\n", " Note(\"4C\", 4, properties={'setdur': 4}),\n", " Note(\"5C\", 2),\n", " Note(\"4Bb\", 1),\n", " Note(\"5D\", 2),\n", " Note(\"5C\", 0.5),\n", " Note(\"4Bb\", 0.5),\n", " Note(\"4A\", 6),\n", " Note(\"4F\", 2),\n", " Note(\"4F\", 3),\n", " Note(\"4E\", 0.5),\n", " Note(\"4D\", 0.5),\n", " Note(\"4C\", 2)\n", "])\n" ] }, { "cell_type": "markdown", "id": "f9780ee8-ecce-41a4-97be-48a0fd0c020b", "metadata": {}, "source": [ "### Contra" ] }, { "cell_type": "code", "execution_count": 3, "id": "626136f9-1a57-4989-95cd-a0ddca0bcaac", "metadata": {}, "outputs": [], "source": [ "\n", "contra = Chain([\n", " Note(\"4F\", 6),\n", " Note(\"4A\", 3),\n", " Note(\"4F\", 3, properties={'setdur': 6}),\n", " Note(\"5C\", 6),\n", " Note(\"4A\", 3),\n", " Note(\"4F\", 3, properties={'setdur': 6}),\n", " Note(\"4C\", 4),\n", " Note(\"5C\", 2),\n", " Note(\"5C\", 2),\n", " Note(\"4A\", 2),\n", " Note(\"4A\", 2),\n", " Note(\"4G\", 4),\n", " Note(\"4A\", 2),\n", " Note(\"4F\", 2),\n", " Note(\"5C\", 4),\n", " Note(\"5D\", 2),\n", " Note(\"5D\", 2),\n", " Note(\"5C\", 4),\n", " Note(\"4F\", 2),\n", " Note(\"4A\", 2),\n", " Note(\"4G\", 1.5),\n", " Note(\"4F\", 0.5),\n", " Note(\"4D\", 2),\n", " Note(\"4A\", 3),\n", " Note(\"4B\", 1),\n", " Note(\"5C\", 2),\n", " Note(\"4A\", 2),\n", " Rest(1),\n", " Note(\"4Bb\", 2),\n", " Note(\"4A\", 1),\n", " Note(\"5C\", 2),\n", " Note(\"4F\", 4),\n", " Note(\"4F\", 2),\n", " Note(\"3Bb\", 4),\n", " Note(\"4F\", 2),\n", " Note(\"4F\", 1),\n", " Note(\"4A\", 2),\n", " Note(\"4B\", 1),\n", " Note(\"5C\", 2) \n", "])\n", "\n", "contra = contra.transpose(-12)" ] }, { "cell_type": "markdown", "id": "876a699d-0737-4dcb-a085-36604503f856", "metadata": {}, "source": [ "Here we calculate the duration of the imitation, using the previously set properties to account for the exceptions to the rule" ] }, { "cell_type": "code", "execution_count": 4, "id": "ba32f51d-8a44-4b1d-8993-1e5c87d913d2", "metadata": {}, "outputs": [ { "data": { "text/html": [ "Score(2 voices)
\n", " " ], "text/plain": [ "Score(2 voices)" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def cantusProlatio(n):\n", " if dur := n.getProperty('setdur'):\n", " return dur\n", " elif n.dur == 4:\n", " return n.dur * 1.5\n", " return n.dur\n", " \n", "cantus2 = Chain([n.clone(dur=cantusProlatio(n)) for n in cantus])\n", "\n", "Score([cantus, cantus2])" ] }, { "cell_type": "code", "execution_count": 5, "id": "19e8bd5f-a775-42c5-9e59-f1a271f99493", "metadata": {}, "outputs": [ { "data": { "text/html": [ "Score(2 voices)
\n", " " ], "text/plain": [ "Score(2 voices)" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def contraProlatio(n):\n", " if dur := n.getProperty('setdur'):\n", " return dur\n", " elif n.dur == 6:\n", " return n.dur * F(3, 2)\n", " return n.dur\n", " \n", "contra2 = Chain(n.clone(dur=contraProlatio(n))\n", " for n in contra)\n", "\n", "Score([contra, contra2])\n", " " ] }, { "cell_type": "code", "execution_count": 8, "id": "98fab5d6-3bf4-4133-8872-21db0a19aede", "metadata": {}, "outputs": [ { "data": { "text/html": [ "Score(4 voices)
\n", " " ], "text/plain": [ "Score(4 voices)" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "struct = ScoreStruct(timesig=(2, 2), tempo=120)\n", "# setScoreStruct(struct)\n", "\n", "origscore = Score([Voice(cantus, name='cantus'),\n", " Voice(cantus2, name='cantus2'),\n", " Voice(contra, name='contra'),\n", " Voice(contra2, name='contra2')\n", " ], scorestruct=struct)\n", "origscore" ] }, { "cell_type": "markdown", "id": "7eed104f-45e3-40ef-a747-220f80e8c6df", "metadata": {}, "source": [ "For playback we will use a very simple singing voice based on the formants of a male tenor. To match\n", "the range it might be a good idea to transpose the score down." ] }, { "cell_type": "code", "execution_count": 9, "id": "72696794-805b-404f-8975-6aaf098907a4", "metadata": {}, "outputs": [ { "data": { "text/html": [ "Score(4 voices)
\n", " " ], "text/plain": [ "Score(4 voices)" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "s = origscore.transpose(-5)\n", "s" ] }, { "cell_type": "markdown", "id": "722ee4ea-438d-4fb7-a277-5279c4089c2b", "metadata": {}, "source": [ "## Playback \n", "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" ] }, { "cell_type": "code", "execution_count": 10, "id": "fd196ae3-4bc0-449c-81ba-be78cf0f147d", "metadata": {}, "outputs": [ { "data": { "text/html": [ "Preset: sing\n", "
\n", " (routing=True, numouts=2, numsignals=1)\n", "\n", "init:
      gi__formantFreqs__[] fillarray   \\\n",
       "          668, 1191, 2428, 3321, 4600, \\  ; A\n",
       "          327, 2157, 2754, 3630, 4600, \\  ; E \n",
       "          208, 2152, 3128, 3425, 4200, \\  ; I\n",
       "          335, 628, 2689, 3515, 4200,  \\  ; O\n",
       "          254, 796, 2515, 3274, 4160      ; U\n",
       "\n",
       "      gi__formantDbs__[] fillarray   \\\n",
       "          28, 28, 22, 20, 20,        \\\n",
       "          15, 25, 24, 20, 23,        \\\n",
       "          10, 20, 27, 26, 20,        \\\n",
       "          15, 18, 5,  7,  12,        \\\n",
       "          12, 10, 6,  5,  12\n",
       "\n",
       "      gi__formantBws__[] fillarray   \\\n",
       "          80, 90, 120, 130, 140,     \\\n",
       "          60, 100, 120, 150, 200,    \\\n",
       "          60, 90, 100, 120, 120,     \\\n",
       "          40, 80, 100, 120, 120,     \\\n",
       "          50, 60, 170, 180, 200\n",
       "\n",
       "      gi__formantAmps__[] maparray gi__formantDbs__, "ampdb"\n",
       "      reshapearray gi__formantFreqs__, 5, 5\n",
       "      reshapearray gi__formantAmps__, 5, 5\n",
       "      reshapearray gi__formantBws__, 5, 5\n",
       "    \n",
       "
\n", "\n", "audiogen:\n", "
  |kx=0.0, ky=0.0, kvibamount=1.0|\n",
       "
\n", "
\n", "
  kx *= random:i(0.95, 1.05)\n",
       "  ky *= randomi:k(0.96, 1.04, 1)\n",
       "  kvibfreq = linsegr:k(0, 0.1, 0, 0.4, 4.7, 0.2, 1) * randomi:k(0.9, 1.1, 3)\n",
       "  kvibsemi = linsegr:k(0, 0.2, 0, 0.8, 0.4, 0.2, 0) * randomi:k(0.85, 1.15, 8)\n",
       "  kvib = oscil:k(kvibsemi/2, kvibfreq) - kvibsemi/2\n",
       "  kpitch = lag:k(kpitch, 0.2) + kvib*kvibamount\n",
       "  asource = butterlp:a(vco2:a(kamp, mtof(kpitch)), 4000)\n",
       "  ;                   x    y    weight\n",
       "  kcoords[] fillarray 0,   0,   1,      \\  ; A\n",
       "                      0.5, 0.5, 0.3,    \\  ; E\n",
       "                      1,   0,   1,      \\  ; I\n",
       "                      0,   1,   1,      \\  ; O\n",
       "                      1,   1,   1          ; U\n",
       "\n",
       "  kweights[] presetinterp kx, ky, kcoords, 0.2\n",
       "  kformantFreqs[] weightedsum gi__formantFreqs__, kweights\n",
       "  kformantBws[]   weightedsum gi__formantBws__, kweights\n",
       "  kformantAmps[]  weightedsum gi__formantAmps__, kweights\n",
       "  kformantFreqs poly 5, "lag", kformantFreqs, 0.2\n",
       "  kformantAmps  poly 5, "lag", kformantAmps, 0.2\n",
       "  aformants[] poly 5, "resonx", asource, kformantFreqs, kformantBws, 2, 1\n",
       "  aformants *= kformantAmps\n",
       "  aout1 = sumarray(aformants) * 0.1\n",
       "
\n", "
" ], "text/plain": [ "Preset: sing \n", " routing=True\n", " init: gi__formantFreqs__[] fillarray \\\n", " 668, 1191, 2428, 3321, 4600, \\ ; A\n", " 327, 2157, 2754, 3630, 4600, \\ ; E \n", " 208, 2152, 3128, 3425, 4200, \\ ; I\n", " 335, 628, 2689, 3515, 4200, \\ ; O\n", " 254, 796, 2515, 3274, 4160 ; U\n", "\n", " gi__formantDbs__[] fillarray \\\n", " 28, 28, 22, 20, 20, \\\n", " 15, 25, 24, 20, 23, \\\n", " 10, 20, 27, 26, 20, \\\n", " 15, 18, 5, 7, 12, \\\n", " 12, 10, 6, 5, 12\n", "\n", " gi__formantBws__[] fillarray \\\n", " 80, 90, 120, 130, 140, \\\n", " 60, 100, 120, 150, 200, \\\n", " 60, 90, 100, 120, 120, \\\n", " 40, 80, 100, 120, 120, \\\n", " 50, 60, 170, 180, 200\n", "\n", " gi__formantAmps__[] maparray gi__formantDbs__, \"ampdb\"\n", " reshapearray gi__formantFreqs__, 5, 5\n", " reshapearray gi__formantAmps__, 5, 5\n", " reshapearray gi__formantBws__, 5, 5\n", " |kx=0.0, ky=0.0, kvibamount=1.0|\n", " kx *= random:i(0.95, 1.05)\n", " ky *= randomi:k(0.96, 1.04, 1)\n", " kvibfreq = linsegr:k(0, 0.1, 0, 0.4, 4.7, 0.2, 1) * randomi:k(0.9, 1.1, 3)\n", " kvibsemi = linsegr:k(0, 0.2, 0, 0.8, 0.4, 0.2, 0) * randomi:k(0.85, 1.15, 8)\n", " kvib = oscil:k(kvibsemi/2, kvibfreq) - kvibsemi/2\n", " kpitch = lag:k(kpitch, 0.2) + kvib*kvibamount\n", " asource = butterlp:a(vco2:a(kamp, mtof(kpitch)), 4000)\n", " ; x y weight\n", " kcoords[] fillarray 0, 0, 1, \\ ; A\n", " 0.5, 0.5, 0.3, \\ ; E\n", " 1, 0, 1, \\ ; I\n", " 0, 1, 1, \\ ; O\n", " 1, 1, 1 ; U\n", "\n", " kweights[] presetinterp kx, ky, kcoords, 0.2\n", " kformantFreqs[] weightedsum gi__formantFreqs__, kweights\n", " kformantBws[] weightedsum gi__formantBws__, kweights\n", " kformantAmps[] weightedsum gi__formantAmps__, kweights\n", " kformantFreqs poly 5, \"lag\", kformantFreqs, 0.2\n", " kformantAmps poly 5, \"lag\", kformantAmps, 0.2\n", " aformants[] poly 5, \"resonx\", asource, kformantFreqs, kformantBws, 2, 1\n", " aformants *= kformantAmps\n", " aout1 = sumarray(aformants) * 0.1" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "defPreset('sing',\n", " init=r\"\"\"\n", " gi__formantFreqs__[] fillarray \\\n", " 668, 1191, 2428, 3321, 4600, \\ ; A\n", " 327, 2157, 2754, 3630, 4600, \\ ; E \n", " 208, 2152, 3128, 3425, 4200, \\ ; I\n", " 335, 628, 2689, 3515, 4200, \\ ; O\n", " 254, 796, 2515, 3274, 4160 ; U\n", "\n", " gi__formantDbs__[] fillarray \\\n", " 28, 28, 22, 20, 20, \\\n", " 15, 25, 24, 20, 23, \\\n", " 10, 20, 27, 26, 20, \\\n", " 15, 18, 5, 7, 12, \\\n", " 12, 10, 6, 5, 12\n", "\n", " gi__formantBws__[] fillarray \\\n", " 80, 90, 120, 130, 140, \\\n", " 60, 100, 120, 150, 200, \\\n", " 60, 90, 100, 120, 120, \\\n", " 40, 80, 100, 120, 120, \\\n", " 50, 60, 170, 180, 200\n", "\n", " gi__formantAmps__[] maparray gi__formantDbs__, \"ampdb\"\n", " reshapearray gi__formantFreqs__, 5, 5\n", " reshapearray gi__formantAmps__, 5, 5\n", " reshapearray gi__formantBws__, 5, 5\n", " \"\"\",\n", " audiogen = r\"\"\"\n", " |kx=0, ky=0, kvibamount=1|\n", " kx *= random:i(0.95, 1.05)\n", " ky *= randomi:k(0.96, 1.04, 1)\n", " kvibfreq = linsegr:k(0, 0.1, 0, 0.4, 4.7, 0.2, 1) * randomi:k(0.9, 1.1, 3)\n", " kvibsemi = linsegr:k(0, 0.2, 0, 0.8, 0.4, 0.2, 0) * randomi:k(0.85, 1.15, 8)\n", " kvib = oscil:k(kvibsemi/2, kvibfreq) - kvibsemi/2\n", " kpitch = lag:k(kpitch, 0.2) + kvib*kvibamount\n", " asource = butterlp:a(vco2:a(kamp, mtof(kpitch)), 4000)\n", " ; x y weight\n", " kcoords[] fillarray 0, 0, 1, \\ ; A\n", " 0.5, 0.5, 0.3, \\ ; E\n", " 1, 0, 1, \\ ; I\n", " 0, 1, 1, \\ ; O\n", " 1, 1, 1 ; U\n", "\n", " kweights[] presetinterp kx, ky, kcoords, 0.2\n", " kformantFreqs[] weightedsum gi__formantFreqs__, kweights\n", " kformantBws[] weightedsum gi__formantBws__, kweights\n", " kformantAmps[] weightedsum gi__formantAmps__, kweights\n", " kformantFreqs poly 5, \"lag\", kformantFreqs, 0.2\n", " kformantAmps poly 5, \"lag\", kformantAmps, 0.2\n", " aformants[] poly 5, \"resonx\", asource, kformantFreqs, kformantBws, 2, 1\n", " aformants *= kformantAmps\n", " aout1 = sumarray(aformants) * 0.1\n", " \"\"\"\n", ")" ] }, { "cell_type": "markdown", "id": "5230ab78-d8ca-45ec-900b-10fadb3ba447", "metadata": {}, "source": [ "Make the voices sing some \"random\" vowels" ] }, { "cell_type": "code", "execution_count": 16, "id": "f5b2c887-dd05-47b8-982e-f4da6a232087", "metadata": {}, "outputs": [], "source": [ "vowels= {\n", " 'a': (0, 0, 1),\n", " 'e': (0.5, 0.5, 1),\n", " 'i': (1, 0, 1.3),\n", " 'o': (0, 1, 2),\n", " 'u': (1, 1, 2)\n", "}\n", "\n", "# Vowel patterns for each voice\n", "patterns = [\"aaeo\", \"auoai\", \"aeaoe\", \"aiuoeu\"]\n", "\n", "# Panning position for each voice\n", "positions = [0, 0.3, 0.7, 1]\n", "\n", "for voice, pattern, position in zip(s.voices, patterns, positions):\n", " voice.setPlay(instr='sing', position=position)\n", " for i, n in enumerate(voice):\n", " if n.isRest():\n", " continue\n", " vowel = pattern[i%len(pattern)]\n", " x, y, gain = vowels[vowel]\n", " gain *= 1 if n.dur >= 1 else 1.3\n", " # Set the vowel coords in advance\n", " n.setPlay(args={'kx': x, 'ky': y}, gain=gain)" ] }, { "cell_type": "markdown", "id": "5f427a5b-b124-4c91-9e64-9f1d6692d86b", "metadata": {}, "source": [ "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) " ] }, { "cell_type": "code", "execution_count": 13, "id": "a4fbe813-d216-4d71-8311-5246f8a2a3bc", "metadata": {}, "outputs": [ { "data": { "text/html": [ "Instr postproc
\n", "
\n", "
    a1, a2 monitor\n",
       "    a1post, a2post reverbsc a1, a2, 0.75, 12000, sr, 0.5, 1\n",
       "    outch 1, a1post - a1, 2, a2post - a2\n",
       "
\n", "
" ], "text/plain": [ "Instr(postproc)" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "session = playSession()\n", "session.defInstr(\"postproc\", r'''\n", " a1, a2 monitor\n", " a1post, a2post reverbsc a1, a2, 0.75, 12000, sr, 0.5, 1\n", " outch 1, a1post - a1, 2, a2post - a2\n", "''')" ] }, { "cell_type": "markdown", "id": "763ec0d1-69a8-448f-9222-2355b1eb97e2", "metadata": {}, "source": [ "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` " ] }, { "cell_type": "code", "execution_count": 17, "id": "ed754dbb-0f49-47b4-bbed-88892264d01a", "metadata": {}, "outputs": [ { "data": { "text/html": [ "OfflineRenderer(outfile=\"/home/em/.local/share/maelzel/recordings/rec-2023-03-27T20:37:57.358.wav\", 2 channels, 58.00 secs, 44100 Hz)
\n", "
\n", " \n", " " ], "text/plain": [ "OfflineRenderer(sr=44100)" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Make sure that the reverb has time to die off\n", "totalDuration = s.durSecs() + 4 \n", "\n", "with render() as r:\n", " r.sched('postproc', dur=totalDuration, priority=2)\n", " s.play(sustain=0.15, fade=(0.05, 0.15))\n", "r" ] }, { "cell_type": "code", "execution_count": null, "id": "a798d4e7-b141-4c3e-ad7a-ec62d4422139", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.7" } }, "nbformat": 4, "nbformat_minor": 5 }