{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Chromatic voice leading in common trichords\n",
"\n",
"*...here the circle is purely contrapuntal, describing minimal voice-leading relationships among chords. But we have just seen that the chain of third-related chords can also be used to model harmonic successions in functionally tonal music. ...Harmony and counterpoint here work hand in hand, creating a unified structure in which horizontal and vertical forces are in delicate balance. Dmitri Tymoczko, A Geometry of Music, chap 7, page 231.*\n",
"\n",
"## Introduction\n",
"\n",
"This notebook is a demonstration of Orbichord, a project to explore topologically non-trivial space of music chords that are global-quotient orbifolds, see references:\n",
"\n",
"* Project page: https://orbichord.github.io\n",
"* Project Github: https://github.com/orbichord/orbichord\n",
"* Tymoczko, Dmitri. \"The geometry of musical chords.\" Science 313.5783 (2006): 72-74.\n",
"* Callender, Clifton, Ian Quinn, and Dmitri Tymoczko. \"Generalized voice-leading spaces.\" Science 320.5874 (2008): 346-348.\n",
"* Dmitri Tymoczko, A Geometry of Music: Harmony and Counterpoint in the Extended Common Practice, Oxford University Press, 2011.\n",
"\n",
"Orbichord comes from combining the words orbifold and chord. It is a collection of python modules build on top of [music21 project](https://web.mit.edu/music21)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Import module"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"data": {
"application/javascript": [
"\n",
"(function(root) {\n",
" function now() {\n",
" return new Date();\n",
" }\n",
"\n",
" var force = true;\n",
"\n",
" if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n",
" root._bokeh_onload_callbacks = [];\n",
" root._bokeh_is_loading = undefined;\n",
" }\n",
"\n",
" var JS_MIME_TYPE = 'application/javascript';\n",
" var HTML_MIME_TYPE = 'text/html';\n",
" var EXEC_MIME_TYPE = 'application/vnd.bokehjs_exec.v0+json';\n",
" var CLASS_NAME = 'output_bokeh rendered_html';\n",
"\n",
" /**\n",
" * Render data to the DOM node\n",
" */\n",
" function render(props, node) {\n",
" var script = document.createElement(\"script\");\n",
" node.appendChild(script);\n",
" }\n",
"\n",
" /**\n",
" * Handle when an output is cleared or removed\n",
" */\n",
" function handleClearOutput(event, handle) {\n",
" var cell = handle.cell;\n",
"\n",
" var id = cell.output_area._bokeh_element_id;\n",
" var server_id = cell.output_area._bokeh_server_id;\n",
" // Clean up Bokeh references\n",
" if (id != null && id in Bokeh.index) {\n",
" Bokeh.index[id].model.document.clear();\n",
" delete Bokeh.index[id];\n",
" }\n",
"\n",
" if (server_id !== undefined) {\n",
" // Clean up Bokeh references\n",
" var cmd = \"from bokeh.io.state import curstate; print(curstate().uuid_to_server['\" + server_id + \"'].get_sessions()[0].document.roots[0]._id)\";\n",
" cell.notebook.kernel.execute(cmd, {\n",
" iopub: {\n",
" output: function(msg) {\n",
" var id = msg.content.text.trim();\n",
" if (id in Bokeh.index) {\n",
" Bokeh.index[id].model.document.clear();\n",
" delete Bokeh.index[id];\n",
" }\n",
" }\n",
" }\n",
" });\n",
" // Destroy server and session\n",
" var cmd = \"import bokeh.io.notebook as ion; ion.destroy_server('\" + server_id + \"')\";\n",
" cell.notebook.kernel.execute(cmd);\n",
" }\n",
" }\n",
"\n",
" /**\n",
" * Handle when a new output is added\n",
" */\n",
" function handleAddOutput(event, handle) {\n",
" var output_area = handle.output_area;\n",
" var output = handle.output;\n",
"\n",
" // limit handleAddOutput to display_data with EXEC_MIME_TYPE content only\n",
" if ((output.output_type != \"display_data\") || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n",
" return\n",
" }\n",
"\n",
" var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n",
"\n",
" if (output.metadata[EXEC_MIME_TYPE][\"id\"] !== undefined) {\n",
" toinsert[toinsert.length - 1].firstChild.textContent = output.data[JS_MIME_TYPE];\n",
" // store reference to embed id on output_area\n",
" output_area._bokeh_element_id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n",
" }\n",
" if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n",
" var bk_div = document.createElement(\"div\");\n",
" bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n",
" var script_attrs = bk_div.children[0].attributes;\n",
" for (var i = 0; i < script_attrs.length; i++) {\n",
" toinsert[toinsert.length - 1].firstChild.setAttribute(script_attrs[i].name, script_attrs[i].value);\n",
" }\n",
" // store reference to server id on output_area\n",
" output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n",
" }\n",
" }\n",
"\n",
" function register_renderer(events, OutputArea) {\n",
"\n",
" function append_mime(data, metadata, element) {\n",
" // create a DOM node to render to\n",
" var toinsert = this.create_output_subarea(\n",
" metadata,\n",
" CLASS_NAME,\n",
" EXEC_MIME_TYPE\n",
" );\n",
" this.keyboard_manager.register_events(toinsert);\n",
" // Render to node\n",
" var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n",
" render(props, toinsert[toinsert.length - 1]);\n",
" element.append(toinsert);\n",
" return toinsert\n",
" }\n",
"\n",
" /* Handle when an output is cleared or removed */\n",
" events.on('clear_output.CodeCell', handleClearOutput);\n",
" events.on('delete.Cell', handleClearOutput);\n",
"\n",
" /* Handle when a new output is added */\n",
" events.on('output_added.OutputArea', handleAddOutput);\n",
"\n",
" /**\n",
" * Register the mime type and append_mime function with output_area\n",
" */\n",
" OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n",
" /* Is output safe? */\n",
" safe: true,\n",
" /* Index of renderer in `output_area.display_order` */\n",
" index: 0\n",
" });\n",
" }\n",
"\n",
" // register the mime type if in Jupyter Notebook environment and previously unregistered\n",
" if (root.Jupyter !== undefined) {\n",
" var events = require('base/js/events');\n",
" var OutputArea = require('notebook/js/outputarea').OutputArea;\n",
"\n",
" if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n",
" register_renderer(events, OutputArea);\n",
" }\n",
" }\n",
"\n",
" \n",
" if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n",
" root._bokeh_timeout = Date.now() + 5000;\n",
" root._bokeh_failed_load = false;\n",
" }\n",
"\n",
" var NB_LOAD_WARNING = {'data': {'text/html':\n",
" \"
\\n\"+\n",
" \"
\\n\"+\n",
" \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n",
" \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n",
" \"
\\n\"+\n",
" \"
\\n\"+\n",
" \"
re-rerun `output_notebook()` to attempt to load from CDN again, or
\"}};\n",
"\n",
" function display_loaded() {\n",
" var el = document.getElementById(null);\n",
" if (el != null) {\n",
" el.textContent = \"BokehJS is loading...\";\n",
" }\n",
" if (root.Bokeh !== undefined) {\n",
" if (el != null) {\n",
" el.textContent = \"BokehJS \" + root.Bokeh.version + \" successfully loaded.\";\n",
" }\n",
" } else if (Date.now() < root._bokeh_timeout) {\n",
" setTimeout(display_loaded, 100)\n",
" }\n",
" }\n",
"\n",
"\n",
" function run_callbacks() {\n",
" try {\n",
" root._bokeh_onload_callbacks.forEach(function(callback) {\n",
" if (callback != null)\n",
" callback();\n",
" });\n",
" } finally {\n",
" delete root._bokeh_onload_callbacks\n",
" }\n",
" console.debug(\"Bokeh: all callbacks have finished\");\n",
" }\n",
"\n",
" function load_libs(css_urls, js_urls, callback) {\n",
" if (css_urls == null) css_urls = [];\n",
" if (js_urls == null) js_urls = [];\n",
"\n",
" root._bokeh_onload_callbacks.push(callback);\n",
" if (root._bokeh_is_loading > 0) {\n",
" console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n",
" return null;\n",
" }\n",
" if (js_urls == null || js_urls.length === 0) {\n",
" run_callbacks();\n",
" return null;\n",
" }\n",
" console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n",
" root._bokeh_is_loading = css_urls.length + js_urls.length;\n",
"\n",
" function on_load() {\n",
" root._bokeh_is_loading--;\n",
" if (root._bokeh_is_loading === 0) {\n",
" console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n",
" run_callbacks()\n",
" }\n",
" }\n",
"\n",
" function on_error() {\n",
" console.error(\"failed to load \" + url);\n",
" }\n",
"\n",
" for (var i = 0; i < css_urls.length; i++) {\n",
" var url = css_urls[i];\n",
" const element = document.createElement(\"link\");\n",
" element.onload = on_load;\n",
" element.onerror = on_error;\n",
" element.rel = \"stylesheet\";\n",
" element.type = \"text/css\";\n",
" element.href = url;\n",
" console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n",
" document.body.appendChild(element);\n",
" }\n",
"\n",
" for (var i = 0; i < js_urls.length; i++) {\n",
" var url = js_urls[i];\n",
" var element = document.createElement('script');\n",
" element.onload = on_load;\n",
" element.onerror = on_error;\n",
" element.async = false;\n",
" element.src = url;\n",
" console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n",
" document.head.appendChild(element);\n",
" }\n",
" };\n",
"\n",
" function inject_raw_css(css) {\n",
" const element = document.createElement(\"style\");\n",
" element.appendChild(document.createTextNode(css));\n",
" document.body.appendChild(element);\n",
" }\n",
"\n",
" \n",
" var js_urls = [];\n",
" var css_urls = [];\n",
" \n",
"\n",
" var inline_js = [\n",
" function(Bokeh) {\n",
" /* BEGIN bokeh.min.js */\n",
" /*!\n",
" * Copyright (c) 2012 - 2019, Anaconda, Inc., and Bokeh Contributors\n",
" * All rights reserved.\n",
" * \n",
" * Redistribution and use in source and binary forms, with or without modification,\n",
" * are permitted provided that the following conditions are met:\n",
" * \n",
" * Redistributions of source code must retain the above copyright notice,\n",
" * this list of conditions and the following disclaimer.\n",
" * \n",
" * Redistributions in binary form must reproduce the above copyright notice,\n",
" * this list of conditions and the following disclaimer in the documentation\n",
" * and/or other materials provided with the distribution.\n",
" * \n",
" * Neither the name of Anaconda nor the names of any contributors\n",
" * may be used to endorse or promote products derived from this software\n",
" * without specific prior written permission.\n",
" * \n",
" * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n",
" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n",
" * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n",
" * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n",
" * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n",
" * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n",
" * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n",
" * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n",
" * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n",
" * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF\n",
" * THE POSSIBILITY OF SUCH DAMAGE.\n",
" */\n",
" (function(root, factory) {\n",
" root[\"Bokeh\"] = factory();\n",
" })(this, function() {\n",
" var define;\n",
" var parent_require = typeof require === \"function\" && require\n",
" return (function(modules, entry, aliases, externals) {\n",
" if (aliases === undefined) aliases = {};\n",
" if (externals === undefined) externals = {};\n",
"\n",
" var cache = {};\n",
"\n",
" var normalize = function(name) {\n",
" if (typeof name === \"number\")\n",
" return name;\n",
"\n",
" if (name === \"bokehjs\")\n",
" return entry;\n",
"\n",
" var prefix = \"@bokehjs/\"\n",
" if (name.slice(0, prefix.length) === prefix)\n",
" name = name.slice(prefix.length)\n",
"\n",
" var alias = aliases[name]\n",
" if (alias != null)\n",
" return alias;\n",
"\n",
" var trailing = name.length > 0 && name[name.lenght-1] === \"/\";\n",
" var index = aliases[name + (trailing ? \"\" : \"/\") + \"index\"];\n",
" if (index != null)\n",
" return index;\n",
"\n",
" return name;\n",
" }\n",
"\n",
" var require = function(name) {\n",
" var mod = cache[name];\n",
" if (!mod) {\n",
" var id = normalize(name);\n",
"\n",
" mod = cache[id];\n",
" if (!mod) {\n",
" if (!modules[id]) {\n",
" if (parent_require && externals[id]) {\n",
" try {\n",
" mod = {exports: parent_require(id)};\n",
" cache[id] = cache[name] = mod;\n",
" return mod.exports;\n",
" } catch (e) {}\n",
" }\n",
"\n",
" var err = new Error(\"Cannot find module '\" + name + \"'\");\n",
" err.code = 'MODULE_NOT_FOUND';\n",
" throw err;\n",
" }\n",
"\n",
" mod = {exports: {}};\n",
" cache[id] = cache[name] = mod;\n",
" modules[id].call(mod.exports, require, mod, mod.exports);\n",
" } else\n",
" cache[name] = mod;\n",
" }\n",
"\n",
" return mod.exports;\n",
" }\n",
"\n",
" var main = require(entry);\n",
" main.require = require;\n",
"\n",
" main.register_plugin = function(plugin_modules, plugin_entry, plugin_aliases, plugin_externals) {\n",
" if (plugin_aliases === undefined) plugin_aliases = {};\n",
" if (plugin_externals === undefined) plugin_externals = {};\n",
"\n",
" for (var name in plugin_modules) {\n",
" modules[name] = plugin_modules[name];\n",
" }\n",
"\n",
" for (var name in plugin_aliases) {\n",
" aliases[name] = plugin_aliases[name];\n",
" }\n",
"\n",
" for (var name in plugin_externals) {\n",
" externals[name] = plugin_externals[name];\n",
" }\n",
"\n",
" var plugin = require(plugin_entry);\n",
"\n",
" for (var name in plugin) {\n",
" main[name] = plugin[name];\n",
" }\n",
"\n",
" return plugin;\n",
" }\n",
"\n",
" return main;\n",
" })\n",
" ([\n",
" function _(n,o,r){n(1),function(n){for(var o in n)r.hasOwnProperty(o)||(r[o]=n[o])}(n(102))},\n",
" function _(n,c,f){n(2),n(11),n(14),n(21),n(49),n(52),n(87),n(94),n(100)},\n",
" function _(e,n,a){e(3)()||Object.defineProperty(Object,\"assign\",{value:e(4),configurable:!0,enumerable:!1,writable:!0})},\n",
" function _(r,t,o){t.exports=function(){var r,t=Object.assign;return\"function\"==typeof t&&(t(r={foo:\"raz\"},{bar:\"dwa\"},{trzy:\"trzy\"}),r.foo+r.bar+r.trzy===\"razdwatrzy\")}},\n",
" function _(t,r,n){var o=t(5),c=t(10),a=Math.max;r.exports=function(t,r){var n,f,h,i=a(arguments.length,2);for(t=Object(c(t)),h=function(o){try{t[o]=r[o]}catch(t){n||(n=t)}},f=1;f= 0\");if(!isFinite(r))throw new RangeError(\"Count must be < ∞\");for(n=\"\";r;)r%2&&(n+=t),r>1&&(t+=t),r>>=1;return n}},\n",
" function _(t,i,n){var r=t(18),a=Math.abs,o=Math.floor;i.exports=function(t){return isNaN(t)?0:0!==(t=Number(t))&&isFinite(t)?r(t)*o(a(t)):t}},\n",
" function _(n,t,i){t.exports=n(19)()?Math.sign:n(20)},\n",
" function _(n,t,o){t.exports=function(){var n=Math.sign;return\"function\"==typeof n&&(1===n(10)&&-1===n(-20))}},\n",
" function _(n,r,t){r.exports=function(n){return n=Number(n),isNaN(n)||0===n?n:n>0?1:-1}},\n",
" function _(e,r,a){e(22)()||Object.defineProperty(Array,\"from\",{value:e(23),configurable:!0,enumerable:!1,writable:!0})},\n",
" function _(n,o,r){o.exports=function(){var n,o,r=Array.from;return\"function\"==typeof r&&(o=r(n=[\"raz\",\"dwa\"]),Boolean(o&&o!==n&&\"dwa\"===o[1]))}},\n",
" function _(e,l,r){var n=e(24).iterator,t=e(44),a=e(45),i=e(46),u=e(47),o=e(10),f=e(8),c=e(48),v=Array.isArray,h=Function.prototype.call,y={configurable:!0,enumerable:!0,writable:!0,value:null},s=Object.defineProperty;l.exports=function(e){var l,r,A,g,p,w,b,d,x,j,O=arguments[1],m=arguments[2];if(e=Object(o(e)),f(O)&&u(O),this&&this!==Array&&a(this))l=this;else{if(!O){if(t(e))return 1!==(p=e.length)?Array.apply(null,e):((g=new Array(1))[0]=e[0],g);if(v(e)){for(g=new Array(p=e.length),r=0;r
\"],_default:[0,\"\",\"\"]};function ve(e,t){var n;return n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||\"*\"):void 0!==e.querySelectorAll?e.querySelectorAll(t||\"*\"):[],void 0===t||t&&N(e,t)?b.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=ie(o),a=ve(f.appendChild(o),\"script\"),l&&ye(a),n)for(c=0;o=a[c++];)he.test(o.type||\"\")&&n.push(o);return f}me=r.createDocumentFragment().appendChild(r.createElement(\"div\")),(xe=r.createElement(\"input\")).setAttribute(\"type\",\"radio\"),xe.setAttribute(\"checked\",\"checked\"),xe.setAttribute(\"name\",\"t\"),me.appendChild(xe),h.checkClone=me.cloneNode(!0).cloneNode(!0).lastChild.checked,me.innerHTML=\"\",h.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return r.activeElement}catch(e){}}()==(\"focus\"===t)}function Ae(e,t,n,r,i,o){var a,s;if(\"object\"==typeof t){for(s in\"string\"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&(\"string\"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return b().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=b.guid++)),e.each(function(){b.event.add(this,t,i,r,n)})}function De(e,t,n){n?(Y.set(e,t,!1),b.event.add(e,t,{namespace:!1,handler:function(e){var r,i,a=Y.get(this,t);if(1&e.isTrigger&&this[t]){if(a.length)(b.event.special[t]||{}).delegateType&&e.stopPropagation();else if(a=o.call(arguments),Y.set(this,t,a),r=n(this,t),this[t](),a!==(i=Y.get(this,t))||r?Y.set(this,t,!1):i={},a!==i)return e.stopImmediatePropagation(),e.preventDefault(),i.value}else a.length&&(Y.set(this,t,{value:b.event.trigger(b.extend(a[0],b.Event.prototype),a.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Y.get(e,t)&&b.event.add(e,t,ke)}b.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Y.get(e);if(v)for(n.handler&&(n=(o=n).handler,i=o.selector),i&&b.find.matchesSelector(re,i),n.guid||(n.guid=b.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(t){return void 0!==b&&b.event.triggered!==t.type?b.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||\"\").match(P)||[\"\"]).length;l--;)d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||\"\").split(\".\").sort(),d&&(f=b.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=b.event.special[d]||{},c=b.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&b.expr.match.needsContext.test(i),namespace:h.join(\".\")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),b.event.global[d]=!0)},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Y.hasData(e)&&Y.get(e);if(v&&(u=v.events)){for(l=(t=(t||\"\").match(P)||[\"\"]).length;l--;)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||\"\").split(\".\").sort(),d){for(f=b.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp(\"(^|\\\\.)\"+h.join(\"\\\\.(?:.*\\\\.|)\")+\"(\\\\.|$)\"),a=o=p.length;o--;)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&(\"**\"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||b.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)b.event.remove(e,d+t[l],n,r,!0);b.isEmptyObject(u)&&Y.remove(e,\"handle events\")}},dispatch:function(e){var t,n,r,i,o,a,s=b.event.fix(e),u=new Array(arguments.length),l=(Y.get(this,\"events\")||{})[s.type]||[],c=b.event.special[s.type]||{};for(u[0]=s,t=1;t=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&(\"click\"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:b.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\\x20\\t\\r\\n\\f]*)[^>]*)\\/>/gi,qe=/"
],
"text/plain": [
":Chord [source,target] (value)"
]
},
"execution_count": 4,
"metadata": {
"application/vnd.holoviews_exec.v0+json": {
"id": "1004"
}
},
"output_type": "execute_result"
}
],
"source": [
"graph, node_to_chord = createGraph(\n",
" generator = chord_generator,\n",
" voice_leading = max_norm_vl,\n",
" tolerance = lambda x: x == 1.0,\n",
" label = lambda chord: chordSymbolFigure(chord) +\\\n",
" '(' + chordPitchNames(chord) + ')'\n",
")\n",
"# Graph decomposition into its connected components\n",
"graph, _ = (graph.subgraph(c) for c in connected_components(graph))\n",
"edges, vertices = convertGraphToData(graph)\n",
"links = pd.DataFrame(edges)\n",
"nodes = hv.Dataset(pd.DataFrame(vertices), 'index')\n",
"chord = hv.Chord((links, nodes))\n",
"chord.opts(\n",
" opts.Chord(\n",
" cmap='Category20', \n",
" edge_cmap='Category20',\n",
" edge_color=dim('source').str(), \n",
" labels='name', node_color=dim('index').str()\n",
" )\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The output of the function is a networkx graph and a dictionary to map node to their counterpart chord object. We also decompose the graph in its connected components to eliminate the **evil permutation**. As a result, the resulting graph is made only of chords and their **good inversions** (for the definition of good and evil permutations, see the guide *Voice leading in C major triads and their permutations*). Convert the chord networkx graph into a collection of links and nodes. Visualize the chord graph using (ironically) a [chord graph](https://en.wikipedia.org/wiki/Chord_diagram)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# The circule of thirds with inversions\n",
"\n",
"## Graph cycle for efficient voice leading graph"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Find a cycle in the graph that starts from a non-inverted C major. This cycle connects chords by efficient voice leading defined as changing one scale degree at the time. The main point is that the resulting contrapuntal cycle is also a **circule-of-thirds** chord progression used to model harmonic successions in functionally tonal music."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.holoviews_exec.v0+json": "",
"text/html": [
"
\n",
"\n",
"\n",
"\n",
"\n",
"\n",
" \n",
"
\n",
""
],
"text/plain": [
":Overlay\n",
" .Graph.I :Graph [start,end]\n",
" .Labels.I :Labels [x,y] (index)"
]
},
"execution_count": 5,
"metadata": {
"application/vnd.holoviews_exec.v0+json": {
"id": "1186"
}
},
"output_type": "execute_result"
}
],
"source": [
"# Generate a graph with all the shortest voice leading between two dichords\n",
"cycle = None\n",
"def cycle_starting_from(source):\n",
" global cycle\n",
" cycle = nx.Graph()\n",
" for path in nx.find_cycle(graph, source=source):#, target=target):\n",
" prev = None\n",
" for node in path:\n",
" cycle.add_node(node)\n",
" if prev:\n",
" cycle.add_edge(prev, node)\n",
" prev = node\n",
" pview = hv.Graph.from_networkx(\n",
" cycle,\n",
" nx.layout.shell_layout \n",
" )\n",
" pview.opts(directed=True, node_size=60, arrowhead_length=0.01, node_color='black')\n",
" labels = hv.Labels(pview.nodes, ['x', 'y'], 'index')\n",
" return (pview * labels.opts(\n",
" text_font_size='7pt', \n",
" text_color='white', bgcolor='white'\n",
" ))\n",
"\n",
"# Demostration for static html notebooks\n",
"cycle_starting_from('C(CEG)')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"An innovation of this graph is to show that the chord progression with all their inversions. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Listening to contrapuntal and minimal voice-leading relationships among chords\n",
"\n",
"Streaming the graph cycle."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAACtCAIAAACiOX7bAAAqbklEQVR4nO2dT4zbxvXHx78kyDqoU9JF/jQpYEvbtE6LIDW1jYEckgLkNkAvBYrlJgjc9lJqD0VRJAdpzz2tdGl6pDaXIJdY2osLuEArblEfAiSGuHHaonWbiLYPrtcXUc4WsAO31e/w4ClL/Vn+Ezmkvp+DIXFl8X2HM49PM/Mej4xGIwYAAAAAAMLwf1kbAAAAAACQPxBCAQAAAACEBiEUAAAAAEBoEEIBAAAAAIQGIRQAAAAAQGgQQgEAAAAAhAYhFAAAAABAaBBCAQAAAACEBiEUAAAAAEBoEEIBAAAAAIQGIVQgHMdxXTdrKwAAAAAgCgihDse27Uqlout61oYAAAAAQBQQQh2Cbduapg2Hw93d3VarlbU5AAAAABCCI6PRKGsbhKZSqezt7dHrUqnkOE629gAAAABABDALNYtGo8HjJ8bY1atXMREFAAAAAIZZqNmUy+WrV68yxiRJ2tzcZIy1223btrO2CwAAAAAZg1moqdi2zeMny7Lq9Tot6mEtDwAAAAAIoabCZ5tarValUmGM0b+dTidLswAAAAAgAAihptLv9xljqqrycgayLDPGer1elmYBAAAAQAAQQh3CxsYGf03zUljIAwAAAABCqEPwVtSk4MmbowcAAACAxQQh1CwkSfK+pSU830EAAAAALCAPZm1AZnQ6HdM0Z3zg1q1b//73vzVN40fef/99xtiRI0e8BwFjbDAYHD169OjRo1kbEpF79+4dHBwcO3bsoYceytqWiECCCNy5c+fOnTvHjx/P2pDoQIIIfPbZZ4yxRx99NGtDolMMCc8+++w777wz4zOLG0Jpmkbbw6ext7dXr9epHBRj7MKFC7u7u4yx1dVVwzDSMDE/rK6urq+v57dZ6Fo3Gg1FUbK2JSKQIALb29vtdrvb7WZtSHQgQQRqtRpjjN998kgxJHz66aezP7O4IZQsy7MnkyqVCtWCkmXZdd1qtUrHa7UaVTcAXk6ePJn3yTlFUSAhc3ItgW7b+bWfQYIY0BQaJGRLkIlM7IWaiizLpVLJsizGWL1epzKbqqoifgIAAADA4s5CBUHTtF6v57ru9vY2Hcn1tCQAAAAAkgIh1CxWVla2trZc16W3tVot19OSAAAAAEgKLOTNQtO0q1evDodDxpiiKJiCAgAAAACxuLNQhxY1uHfv3kcffUSvH3zwwQceeMBbZjMOn3322b/+9S/GWK7zt32cO3eOP1UwdwwGA8ZYrVbLbyI0JIgA5e/keq56kSXcunXr4ODgzp079+7dY4w99NBDR48effzxx9PPzKcCzpDAspZw6EkXN4Q6tKjBL3/5y4ODA3q9ubn58ssvxzzjwcHBe++995vf/Oall176whe+wBjb39//0pe+9Nprrx07dizml2fL7u7umTNncl3U4KOPPnrttdfym04PCSKwvb19/fr1XE9XL6aEixcvvv322/v7+9/73vfeeOMNxtje3t7bb7/9ySefXL9+/fTp0z/5yU++9rWvzc1kPxEqAkBC4tRqtcOLHY7AJHzRQL/fj/mFvV6vVCrRVfEe73a7a2trMb88c8Z15QvKgu52u1kbEh1IEAG6bWRtRSwWUIJ3OcLr6geDAX8WhSRJvV5vDsZORlVVVVWDfx4S5kEQCdgLNYFWq8VT8AzD4KUN4lCtVqksgjcqb7Vauq7v7Oy0Wq2Y3w8AACAC9Xqdvy6Xy/y1LMu8HOBwOJy98SNbICErFnchbxq2bfNraRhGq9Wq1+u9Xo9fxQi0Wi1aGJYkybt6WK/Xaa96u92O8/0ALDKdTqfX6zmOQ8mzsiyXy+X19fUclXCDBEGwbdtr8OrqarPZzNCeCEBCmmAWyk+1WuUpeI1GgzG2srLS6XTifCd/1IDPm9CJAADR6HQ6dJ92XbfT6ViWtbm56ThOs9lcWVnRNE38FAdIyBzy88TW1pb3T9xySZI2NjZSNSsMkJAZ6awp5gV+5SRJ4suxlCgUZwmWL+X6FlZpzZ4xZppmLLuzhmEvVNYsoAQBN08UYBfOAkoYjUbtdpv2qjLGDMMYDAaj0WgwGNBBVVXTtH8UfiPRCBLmQBAJR0ajUfT4K89MLGpw8eJFKjewvLx88uRJfvzSpUvHjx8/fvz40aNHD9+iPwY9n5gxJsuyL9uoAI+zZozt7u6eOHHiq1/9aqj/JU7y6mAw+Oijj06fPh02nR4SEiSsBD5aGWOqqnr/9Omnn16/fp1eP/XUU88++2yypk6DzuszZgaQMA/CSuDQWCCfzLJOp2eMRUhN/fvf/3779u07d+7Qnf2BBx54+OGHv/KVr3z5y19O3sqZFEPCo48+eu3atRmfWdy9UONFDS5cuECxzunTp/nK64ULF957772Dg4ODgwNyB88888yrr74aqsYBD6FOnjyZ62zhaYQtaiBa8mqEdHpISJywEj788MN//vOf9PoHP/iB19S9vT2+o/Fb3/oWqUuBsOn0kDAPClCXIdmKAH/5y18efvjhXBc1yEoCihqEgEcAfClhRkxgGEbwb562kFcYWJiFPAFn/guwhLTgEnyVQbwr8iIvIUHCPChAXYYFqQjgJacSsJ38v9CeNUVRqKCqt7QBrcXyZVrG2Pb2tjcJczaHVmgN/lUFIKfJq14gQQSq1SrfPLGzs1OtVikXzHVdMltVVcuyRM4IgwSQCAUYzjmVgBDqv9Da7fr6Or2ln1CKovT7fcdxSqVSo9Ho9/t8D3iz2XQcJ8g3r66u0ouJmSmu60aoO9XpdOr1uq7rmqZpmqbrer1eFzzzZRyfwbyhcgQkZIiu647jtNvtWq3mOA4Nh2q1qut6r9fLxZ2bJLzxxhsvvPDC+fPnl5eXjx8//txzzz322GPvvvsuJKQJnKog5ElCOhNiuYAahNYRKAtPkiRKChiNRmtra3ytigfCwVev+PYO/oWcWq0WKp2N/+bji4ndbpd/f/ppC6MYC3mCzPwXYAlpASUISJxcMEGGMyTQkWwlxFkFE2Q4L4gEhFD/hTI4KMQhb+4NC2q1mrc16cOKogT88mkPeDFNM9S2KgEXjEfhixqIlrwa4eYNCYmzgCGUgMMZEvhxwXfh+BBtOC+IBBQ1+C9//etf//GPf/BUWF+i/mAw+NOf/sQT8W7duvXnP/+ZjeXxzuDevXvXr1+/cePG448//tBDDzHG7ty5c/To0VC1AARMIWbxihqIkEIcuSKAOPm3iyxBnLoMC1gRABLmQeSKAOI4VRQ1KDgTixq89dZb3/3ud+mq37t37+bNmzwn8+bNmz/60Y9++tOfHjt2jI7QAm2E1FnqW4yxZ555hn9bQARMIWbhixqIRrIVATLJv4UEEeoyLGBFAEiYBxEqAogGihosIpIk8QWpXq/HGNva2vL+1bvKwLIoUiDggvFo8aqTCzjzDwn8eE6XkAQZzpDg/ZPIu3AEZ0GKGiCE+h9qtZp3CzldVL5XSVVVHlGRu/cGWKkh2oLxaPFCKD6kxz01T9hkIYuHxQQSvGQloQCPFoEEESQsYAgl4HBGXajQNBqNcrmsaRqVNqlWq71eb3l5mf5aLpcpU48xZpqmJEm8XkWaIIVYKPKUfzsFSMgQ1GUQgQJIIOBU0yadaC5HDAYDRVEURZlYfYBiUpqdarfbWRgoXP7tKEZGniASFrAiACTMgwLUxS5ARYACSIiTzpZTCQIOZyzkRWQwGKiqSvuivBes3W4rirK2tsYYM00zE9sEXDAeLd4DXkbizfxDgggSFjCEEnA4F0DCgmwk8iHacEZRg1mMFzXwcePGjRs3bhwcHPiOP/XUUydPnjx8o/58EDD/loUsaiCghJjp9CKkEC9yUQNxJIRNp+egLkOCFEBC2IoABZDAEcepoqjBLMaLGkzk4ODgk08+odc3b95866233nnnnTmbNgsB829ZyMxVMSUsIKKlEEeg2BIyqcsQlgIMZ0gA0/A+rW8yKcyGFYnMW0zABeOwFEBCARBw5j8skCACBRjOkAAigxBqAu122zAMwzC8To3IPIQaibdgHIECSMg7AqYQhwUSBKEAwxkSQDQWdy/UNOr1erPZpNeSJNm27Z3KO3JElBZ7880333///WvXrtHmiUceeeTpp5/+2c9+dvbs2axNC0oBJHQ6nV6v5zgOVcGQZblcLq+vr4ufAi3L8nA4pNe9Xs9rsGVZPIvYMIxWq5W+eUGABKGgscDT0XM0FjiQAEKTdQwnFrzsE8ebaEb1yjM0jxAteTUCkJA5BZj5hwQAQLZkHxAIBaVVTwuhTNMslUoZmjcq3OYJSMiQAsz8QwIAIENEWZYShyNHjnjfdrtdTdPoNdUiz2RG3TvhH5ZardZoNBI1JzSO41QqlUWW0G63dV1P1KJkKMDMPyQAADJhcYsaTGNtbW1nZ0dRFFmWNzY2ePxkWdb29na/38/EKv7MGW/9j29/+9veUhlUVodee+t/8AfUZIgsywsu4fDk2IzQdV3M2C44kAAAyIasp8GEo9/vS5Lke8BLr9ejYuUZGkYUYPMEJAAAACgACKEm0Ov1SqWSJEmGYdRqNXqiiwjxE1GAzROQAAAAIO9gL9RkXNfl+eqVSmVjY0O0hZgCbJ6ABAAAAPkFIRQAAAAAQGj+L2sDAAAAAADyB0IoAAAAAIDQoKhBvrFtu91uO45TLpdXV1c1TWs0GrquZ7tzy7Is27b548Fjout6q9WSZZnepizZsqzxgqtevAWrsjV1GuMSVlZWaNtWJv1kYpOurq7OsMdxHJ4FmfhWs5deeun06dNLS0v8yPzaZ7xLdDodqpeRoVUBCTUWBCfI2NR1/fXXX//ggw9mfE86kpP1qEwkaV7y2MEwC5Vj6vW6ruvLy8sbGxsrKyv0dnNz03GcbA0zTZPn9sfEtm3HcXhQkpXkTqfTbDa9Z7Esq9ls8scpimPqNLwSer1et9utVCrlcpnvhc/QHsZYu91eXl7mlbd8yLK8urrqOE6z2fR+wLbtiZ8Pjm3bN2/eXFpaSqF9fF1ia2tL1/X19fXx20aaVoVlfCzYtu0bC4ITZGzScD527BgTQHKCHpUJJm2cIM5WILJOCQQRMQxDkiTf00XoGW3dbjdDw/hzBk3TjP9ttVqNf0+GklVVHT8LVbsQzdRpjEsgk7KqXzVuD90k+DMHx6nVar7/wmJXG+FXbd7tM94lRve70LiE1KyKwMSxQJcmE3vCEnBseodztpKT9agjkaRNJIizFQcRbQKHQosa456Xfs5mG0JtbW1Rd1dVNf63SZJEJZeylewb1fSCStWLZuo0JjomMimTJz+O20PGzOg24yFUu932lsCNAL9qc22faV2CHqo4fjwdq6IxcSzQbT4Te0IRfGzySzDKWnKyHnUkkrSJBHG24oCFvFxCjmB1ddV3XNM0Xu8xK0zTbDQapVJpd3d3xqIVre5Pe0t0Oh1N02hpTCjJZEa5XOYPFRbW1NlomiZJ0tWrV8cbP8gFmgd8JZRj27ZlWRM/rOv6+OdZYOO9V20iM9onFNO6hCzL4891Sc2qRCBRsizTnc9HVr1oGgHH5uxLMFty4gT0qCy8Ux0nZWlBGHe2QoEQKn+4rru3t8emPHat0WhkuJfcsiza7rqxscEY867ft1qtSqVy5MiRer1eqVTq9frKykq1WnVdt1KpbG1trays+DbVdrtdGj9CSfZuaeQbccQ0NQi0Nbvdbke4QAniuq5pmpIkbW5ueg9qmqZp2tbWVqVS8d4/yEiylkXqXcxz1WbA2yeyNMdxqEtM3AW/ublJgyVlqxLBOxYozM22F80m+NiccQnGJc+VGR6VxXOq46QsLQgTna1YZD0NBkLDN59mbcgEDMOgVXaaCvbOGI/uW863cZA7KJVK3rd8kZ4eluL9jxlKpt9kiqLwn6revwpl6jQmLgnx47S0EeoCJWKPoiiqqtJOFEVRfFuFaOWOzk7PqfRKoD7GF2XCGu+9agHbJxqhukRqVkVm9lgYpduLQhHwQvguwSiA5Pkx26OOYjhVIkNp0xDQpBlgFgokBj0Vp1qtMsZkWTYMYzgcdjod38eq1Sr9hqafR5qm0Vv6j7Tmze5POKdp/6E0Gg3btg3D8B0X0NQ4BLxAidBoNGi5od/va5q2vLzszdxuNpuqqtLZK5WKr5EnLkYENz7bq2bbtva/0JpLXvrStLHASbMXJcu0S3Co5MQJ6FFZbKeavrRDEdCkiSCEyh98OWDiurjjOFmlzbdareFweOQ+29vb7P7mg4mQEH4j9N0RvRPO4kiWZbnVavkOimlqQGh6fHl5efxPsy9QspTL5UajoShKs9mkRQT617v4FWoN9FDjg6yXsZntExButnclolwub25uViqV3d1dek0fS82q+PjGgmVZE/t2mr1oNgHH5oxLEFByUoT1qCyMU/WRsrQgTHS2ooEQKn/IskwLHxM3ZpqmmdWGTdM0fYnWpVJpb28vgj2u61qWxXfaiiaZFpgIwU09FNodIsjMBw8j5n0i31WbQfz2KZfLtCTh3V9C5TT57l3a4ZumVUnBxwJVqszWmNkEGZtBLkFqkhP0qCxYnxfwanqdrYAghMoltOV2/E5DE79BXHDiWJYly7JvwyxtgZz9s2kifPqaI5Rk7yZHwU2dDf3IMwxDkE3u3skV6kteP56gTx+/ahNJqn2ow4x3Cd+RlK1KBJJGfTtrWw7n0LEZ5BKkIzlZj8qC9S4Br6aAFcm9IITKJbquG4axvb3tned0Xbder3sTmtJk4qimEdvpdCYmU8zIsDBNc3193XtEQMlEjkz1YVlWvV5XFGWak0o5BabVau3u7iqK4t38wRcUbNve2dkJ/m2zjR+/auMc2j7BmdglbNtutVreVO2UrYqDt3ld161Wq8PhcOIinVCJVIeOzRmXILjkRIjgUVlIpzrxf6UgrVCku3sdJEm73S6VSoqi1Go1wzBUVc0kz6XX69H0OGOsVCpxG2q1Gk+pKJVKZ8+epY+VSiUqjzvt7YsvvjitbGD6kk3T5CVSJEnynbHf74tj6jS8EkqlkqqqqqrSC69JM66I723MEn/jTaqqqiRJ1FbehKPBYEB/oqw9mtKnPuazJ5TxvqsWsH3iY5qmt0soitLr9QzDoMS6rKwKZf/4heNjn+4mqfWiOEwbm+PDOYjkxAnoUX2jIKxTzUTabGY7WzE5MhqNgkZbQEho/X58yje/uK7rOM4MOeJIzpGp+YXaMMF9P4detbni6xKu67quWy6Xs7VqARkfmwW+BAWWli0IoQAAAAAAQoO9UAAAAAAAoUEIBQAAAAAQGoRQAAAAAAChQQgFAAAAABAahFAAAAAAAKFBCAUAAAAAEBqEUAAAAAAAoUEIBQAAAAAQGoRQAAAAAAChQQgFAAAAABAahFAAAAAAAKFBCAUAAAAAEJoQIZRlWY7jzD4CAAAAALAIBAqhWq2Wruurq6s8YBo/AgAAAACwOBwZjUZBP3rkSLfb1TRtxhEAAAAAgEUgm71QjuO4rpvJqQEAAAAA4pNBCGXbdqVS0XU9/VMDAAAAACRC2iGUbduapg2Hw93d3VarlfLZAQAAAAASIe0QqlqtDodDer21tZXy2QEAAAAAEiHVEKrRaOzt7fG3V69exUQUAAAAAPJIoIy8VqvV7/ebzaaqqpVKpdFojB8JcrJyuXz16lXGmCRJm5ubjLF2u23bdkwNAAAAwMLS6XR6vR7P05JluVwur6+vVyqVrE0LQS5VjNKi1+vRGSVJ6vV6o9Go2+0yxvr9fmo2AAAAAIWh3W6XSiXGmGEYdKTb7SqKQndbVVXpbis4+VWRXghlmiY1R7vdpiODwYAxtrW1lZoNAAAAQDHgd1XfZMRgMJAkyTdnISy5VpHeXqh+v88YU1WVlzOQZZkxxmenAAAAABCQer3OX5fLZf5aluVqtUqvh8OhN0YRkFyreDDl821sbPDXtAsKj4gBACwOudzwMYliCCmGCna/4CJ/u7q62mw2M7QnGvlTkdp8V61W852u3W6nbAMAAGRFfjd8+CiGkAKo8E7MrK2tef/EawYJuwTGybWKVEMoSZJ8R6hpUrMBAAAyIdcbPrwUQ0gxVIw8gSDFgoPBYDQaDQYDOpiLQHCUZxUhHjM8G+9y5kSuXLny4Ycf/vjHP+ZHTNO8ffv2iRMnXn311URsEIr9/X3G2JNPPpm1IbEYDofD4fDkyZNZGxKLu3fv7u/vP/nkk0tLS6H+45UrV/b392/fvn337l3G2NLS0he/+MVTp05ldVmvXbsmSRJ38cERSkjkTiWUirCd6le/+tXnn39Or+nXI+cPf/jDpUuX6PXzzz//yiuvJGvqbMJ2KjGFhPW3YqqIOTRu3bpFb/M1NDi///3vb9y4cfv27f/85z+MsQcffPDYsWOKonzzm9+cj6WHsL+//41vfGN2bJPeXqilpSV+gRljly9fvn37NmPsiSeeSM2GNLl48SJjLO/R4eXLly9duuRzMbljf3+/3W6vr68H901Xrly5ePHi7du3n3/++e9///uMsWvXrl28ePFvf/vbpUuXTpw48fLLL6fvntrt9gsvvPCd73wn+H8RUEiETiWgigidyvt/vdaePHmS37PTJ0Kn4ogjJI6/FUdFZH976tSpU6dOzcOkCMT0tz/84Q/Z/QF+8+bNCxcu/PnPf87E3168ePGjjz46ZHootfkuKmHgm6BjjAk7QRcTVVVVVc3airiM72DLI1SBrNvtBvy8sJP8jLFarRb882IKCdupxFQRp1MJteEjTqcSR0hYfyumCvhbcQb4KFinSq+ogSzLpVLJsizGWL1epzLlVNw8NRsACEKuk2y9FENIMVRUq1W+4WNnZ6darVIKmOu6ZLmqqpZlie8PiyGkGCqKQa4HeKpFDTRN6/V6rutub2/TEXrMS1IUIz0VKoQif0m2UyiGkFyr0HVd13UaGrZtU4U8WZZ1Xc/X0CAhb7755vvvv3/+/PmdnR3G2COPPPL000//4he/OHv2bNYGBqIYKgj428xIZ0KMME2zVCrxqblQs8ezETA9NcJCnoAqIkwsC6gCay5CCYmzkCeOirCdSlgiuGIBxzj8rTgqCuNvg3SqVEMoKlBOKIpC+6LiI+ZKapy1eXFULOa2lZGoSbZx7nbiCIlznxBHxcKGUGKOcfhbcVQUxt8G6VTpFTW4e/fuuXPnKCnv4YcffvXVV5PaYC9meuq5c+dYmAwRMVXQqYNniIip4tq1a9GSp4RKFWaMNZvNaMlTQmULh+1UHKFUxOxUgpRmYOE7lZhjHP5WHBWF8bfnzp179NFH//jHP874THp7oS5evMib5pVXXplTo4iTnhoHqBAEoVKFoyFmtnBYiqdCkNIMMSnAGGdQIQy59LepzIeNDMPwntQ75RgfMVdSFzPJVkwVWHNhIs3zL+ZqhZgqRkXZYAd/K46KwvhbUfZCeS+zYRilUsk0zWRPIeBKapztjeKowLYVoQh7t/OWnPb9yTvtzzeipkPYTiWmirCdSkwVo6JssIO/FUdFYfxtkE4194U827b5NinDMFqtVr1e7/V6vN5DIhQjW7gYSbbFUEEUJlWY5TFbeBJQIQLwt+JQDBVELv3tvOM4nl3JU/Da7fYiPFoYSbbiqIiTISKOilFR1lwWc7VCTBWjSLNQAgJ/K46Kwvjb7BfyvK6BL//Tk16K+lwXDpJsxVGxsNtWRkLO8y/saoWAKkaLGkKJOcbhb8VRMRKhqAFPuXz55ZfPnDnDj7/zzjsnTpw4efJktMfOT0OobGEk2YqjImySrZgqWIyiBkJlCy94UQOhVLDYnQr+NkHgb8VRwTIvanD58mVqlxMnTvD46fLlyx9++OHt27dv3bpFTfPEE0+cOXMmZipjwbKFC5CeyqBCJHKZLeyheEUN8quCwd8KCVRkw/wmwXghAz6h5ytt4CVOHoqY04BIshVHBbatCAVWK8RRMSpKpQz4W3FUFMbfZrwXivaCKYpCb73NRGv/fFsAEfneIGa2MJJsxVGBbStCgaIGvj/lq6iBmELgb8VRURh/G6RT/R+bG3t7e4yx9fV1ekvhpKIo/X7fcZxSqdRoNPr9Ph91zWbTcZyYJ7Vt2/t2dXU15hemia7rjuO02+1areY4jq7rmqZVq1Vd13u9nmVZQud23odUvPHGGy+88ML58+eXl5ePHz/+3HPPPfbYY++++y5UZEKn06nX69SjNE3Tdb1er/sGSy7I9QDnFEMFy7mQIvlbqMiM+UVw9P0UilIWniRJ/NHCa2tr/KcPn6CK9gtbzGnACL+KBARJtuKoGMXLyBNHCFYrxFExKkqljIX1twKyUKU153i1VFVljFHMRG3qHai1Ws1rHH2Yr/qFRcBpwMUc0mLuk8C2FaGEYLVCHBWjolTKWEx/KyYLFULNsajBb3/7248//ti7TudNnb127dr58+d//vOf09srV678+te/ZmM5jaEQKls4bJItR6hUYSTZiqOChc8/F1NI5KIGQpVmKMzj6ItRKSOyvxWKyENDKCIPDdHIuKjBE088wRi7du0atePXv/7169ev879KkvT555/fvXt3aWmJMXbq1CkKoaJRjGxhpAoLSDFUsEIIyXtpBqIYKliBhAAQnbnOg0mSxKeIe70eY2xra8v7V+9cH2Ms2kysmKsVqJYrzj4JbFsRSghWK4QibKcSEyzkiUNhhkbGGXmMsWq12mq16JGBlUrFNM3NzU3+gOFKpcITOizLYlETOrxriOVymb+WZZmfazgcem8nAlIMFdVqle+T2NnZqVardPVd1yXLVVUVOr2CMVYUFaxAQgAAQEDmuJDHGGs0GpZlaZpmWRaFApVKhaIlxli5XKZMPcaYaZqSJPFYITJ5fwQ6kWsVRXqKe95VsAIJAQAA0ZhvCMUYoxCKR1GVSoV7bVmWaRaq1Wrt7Oy0221ZliOcotFobGxs0Outra1Op8P/xGe5JEninxGTYqjg0J07ayviUgwVrEBCAABAHOa7kMcYk2WZgqdyueyr5reysuK6rq7rGxsbpmlGdvHFWK0ohgoAAABgQZhjUQMfly9f/vjjj3kGLOf5558/c+aM96EBkSlGkq1QKpBkKxSR88+FAp1KKIrRqVDUQBwKMzSCFDVIL4Qi7t69u7+/T6+Hw+Hvfve7vHcXAAAAABSPlZWV2etjiYVQ0ThyJGMDAAAAAAAiMPft5IyxTqdDhSI2Nze96foAAAAAADll7pNA9XqdJ+RLkmTbtjeKwiwUAAAAAPLIfDPyXNf1FjTyVYb0ZucBAAAAAOSI+YZQs4Mk27b5474BAAAAAHLEfEMoTdN8R7yPcOn1euMfAAAAAAAQn7lvJ19bW9vZ2VEURZbljY0NHjNZlrW9vd3v9+dtAAAAAABA4sx9N7fjOJVKpVwuU41yOmjbtqZp1Wq10WjM9ewAAAAAAPMgjYQ4ergpPctFlmXHcXZ2dmq1GuInAAAAAOSUlGoKuK5Lz4qnSamNjQ0UiAIAAABAfhGxLJNlWVSKk7OyskIPKk4q8HrppZdOnz69tLQ07QOhZsjGDWaMra6uTjPYcRxe3GF9fT3ZJwfruv76669/8MEHMz6TzvyfbdvtdttxnHK5vLq6qmlap9ORZTn9HIKJF8hLzAbRdb3VannXqbMSPn7qRqOh67qvH3oNFucyjWNZlm3bAR8eJQ6h+puv87DUVc/b36YmMKwfZnN2xXMi4ICddiOY3SBJMW+XO/EOno40H/PNyItDp9NpNpuO4zDGer1et9ulPVXxq0nZtn3z5s2lpSXvKQjLsprNpreWVTSDGWPtdnt5eVnTNNd1fZ+UZXl1ddVxnGaz6f2rbdvjHw6FbduO4xw7dmzcHhZPXVjq9bqu68vLyxsbGysrK1tbW7qur6+vzx5Xc2W8QWzbjt8g1Ob8DpGhcN+p6e3m5qZXss9gAS+TF9M0t7a2srYiIkEGoK/zEJmonpO/TV9gcD/M5umK50TAATvjRjC7QZJlfi534h08TWn/ZSQqqqoyxrrdLj8yGAwURZEkqdfrxfnmWq1mmubEU4xGo7W1tWjNMv5t5CYMw5hmhu/zjLFarRbh1N7vJGkT7RnFUBcKwzAkSer3++OnjikwDhMbhK5CnK/1tnmGwsdPTeNlomQyWMzLxBkMBuSjePPmiyAD0Nt5iKxUz8nfpi8wrB8ezccVz4PgA3b2jeDQBkmKebvcDKVxxJ2FGkeW5UajMRwOZz85+VBardbEb7AsiyW6yEXTwr45gBm02+3Nzc04Z5wmjc1B3Qwbtre3q9Wqb0K11WpJkjTvsweHGiRmgzNPm2cofOKpabxMM1j8y9Rqtej20G63s7YlASYOwPEBK47qRPytCALD+mGWhCtOnFADdsaNgEVqkERI3OWOk760PIVQjDFN0yRJunr16vj0Mq2vT3vL6XQ6mqb5JpYJKvtZLpeTvYX4zmXbNvWkcShjceKfgqibIY3NTd04tLfAW0OVkGU5ZuybLGShLMv0U2acsG2eofBpp9Y0zfcAAG6w+JfJNM1Go1EqlXZ3d2f4xIADP3PGB+DEARtQdTrE9LdCCRx3jBFccYadLfiAnX0j8P7HZC08lNkuN/4dnJOqtNTmu8IycQ6QH6epPFqnqNVqiqLQa8MwaP6ZPqaqqu+/G4YxcYaTJgATNHgwGKytrXmnwQeDgaqqkiSpqqooCv0Uo8+TBOaZj42gzistcXUB4bVSB4PBxL/6ZqHT5NAGidPmGQrnKyMTT9Fut73HyWCRLxPR7Xapqcdn5qMN/PQ5tL/5Buxopuo0rR0/Hs3fZiLwUD88CuOKBelsoQbsjBvBaEqDzInZQyDZO3jK0jg5DqFGoxHtoeNNRhepVCp533o702AwKJVKvq9SFIX/Uo9vMF176g2Konh7Nq0Bk229Xo9+jHovP/vfJe1Q6nzSElcXEL6rMYVzhSVIg0Ru8wyFBz81N1jky0RwR0njQpIk780j7MDPhNn9bXzAjg5TnYK1CfrbrAQe6odHIV2xCJ0twhjnBGmQOXGoy03qDp6+NE7OFvImUq1WaQWUsjo1TaO31WqVMeZ9hgzNAfr+e6PRsG3bMIxEjGk0GjQD2e/3NU1bXl7mubvNZlNVVbKtUqn4LJk29xhQ3URpiauLBhWj95LhUkuQBonZ5hyhhLPDDBbHWiojR60ty7JhGMPhsNPp+D4WfOBnyLT+Nn4tAqrOnMijI02BM/wwi+SKBexsEwfsjBvBjAaZK4e63Ph38KyksdzthWKMUb7i8vLy+J+o3fkAGB8J3W53fC2ZPtlqtZK1s1wuNxoNRVGazaZlWbTo7q07ErZ8xWx106Sx+aibBhflTSstl8ubm5uVSmV3d5deZ1tY1dcglmVN25MRvM0zFM471UQVjuPw49xgwS9Tq9UaDodH7rO9vc3u7wWZyKEDP1smDsDxARtWdTpE9reCCPT5YXZ/U3NkV5xJZws+YGfcCPj/8jVICgR0uZHv4EQm0vIXQu3t7bH74WooXNe1LGvGVlma3U0W6vrzrrJzqDQ2H3XjlMtlmrP19mAq+8b3EgbZ6pgCvEGoTl3Y/+5r8wyFy7JMk9gTJ41M06TjXoMFv0ymafp2M5RKpb29PTH3iQfEOwAnDlgxVUfzt6IJTMcPz4+AA5Y+ECQdJJMGSdblTiNlaTkLoSiSNQwjwu9jPoc8jXkk/PPfcOPJlglmoxwqjaVVkZyfaLwHi+a/yE5aXIjw38fbPEPhlCc8fiJSR07HZ7Cwl4meR+6rE72xscEEmJKJg3cAjnceMVVH9reiCfTOpc3VFc+PIAM2yI2AmDG5OD8Sd7kTSVlankIoy7Lq9bqiKNOigdk1SU3TXF9fn49pk2m1Wru7u4qiVKtVWvvnE5i2be/s7IT6thnq0pc2A13XDcPY3t72ztzati1IwSFvM7quW61Wh8PhtOmWUG2eofCJp3Zdt16v8yosPoOFvUwTb6jkOjudzsQrImYh6RmMd54IqudNHH8rlECvH2b3t2FFdsVZdbYgAzbgjcDXIPMmuMuNfwdPWRpjQqbkmKbJ60aUSiVVVVVVpRfezfk8JbJUKnlzbsffqqra7/e9O/m9p6DU1jgpFePfRhmziqLUajWecuLLpKVZzVKpZJqmz9pQ6l588UVfCkay6iK3SalUIpmGYSiK0uv1DMPIpObvxAtEDegdBXHa3HuurIS3223vqb3X3df/RbB2nF6vxy8KjQs6XqvVeEZPqVQ6e/Zs8IGfvorRYQPQdy0Cqp7fEE7c3/pGR2oCA/rhURhXHOouE9P+gBqnDdjxMR68QeZh56EuN9k7eGrSfIj4mOF54Lqu4zgiPEWSnr6U4DNcxZE2DonlE/iu67qum+1e8kQ4tM0zFO47dRCDi3qZxETkAZsIeRGYuCtOjYkDVpblXDR7NITtVIsSQgEAAAAAJEie9kIBAAAAAAgCQigAAAAAgNAghAIAAAAACA1CKAAAAACA0CCEAgAAAAAIDUIoAAAAAIDQIIQCAAAAAAgNQigAAAAAgNAghAIAAAAACA1CKAAAAACA0Pw/EYZuGCqPkYsAAAAASUVORK5CYII=\n",
"text/plain": [
""
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"stream = Stream()\n",
"interval4 = Interval(4*12)\n",
"for node in cycle.nodes:\n",
" chord = node_to_chord[node].transpose(interval4)\n",
" chord.addLyric(chordSymbolFigure(chord))\n",
" chord.duration.type = 'whole' \n",
" stream.append(chord)\n",
"# Render the resulting chord progression\n",
"renderWithLily(stream)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
" \n",
" "
],
"text/plain": [
""
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Play the chord progression\n",
"playAudio(stream)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "orbichord",
"language": "python",
"name": "orbichord"
},
"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.8.1"
}
},
"nbformat": 4,
"nbformat_minor": 4
}