From 55ad8246b0e6ea5cb8c223aa853795a947e635b4 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 7 Oct 2022 12:40:30 +0200 Subject: [PATCH 01/41] updated requirements for docs and moved to docs source. --- README.md | 4 ++-- docs/requirements.txt | 3 +++ docs/{ => source}/Makefile | 0 docs/{ => source}/Tutorial/notebook.ipynb | 0 docs/{ => source}/conf.py | 2 +- docs/{ => source}/genindex.rst | 0 docs/{ => source}/images/aknowledge_logo.png | Bin .../images/aknowledge_logo_negative.png | Bin docs/{ => source}/images/erc_fwf_logos.jpg | Bin docs/{ => source}/images/score_example.png | Bin docs/{ => source}/images/score_example_1.png | Bin docs/{ => source}/images/score_example_2.png | Bin docs/{ => source}/index.rst | 0 docs/{ => source}/introduction.rst | 0 docs/{ => source}/make.bat | 0 .../modules/partitura.musicanalysis.rst | 0 docs/{ => source}/modules/partitura.performance.rst | 0 docs/{ => source}/modules/partitura.rst | 0 docs/{ => source}/modules/partitura.score.rst | 0 docs/{ => source}/modules/partitura.utils.rst | 0 requirements.txt | 3 +-- 21 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 docs/requirements.txt rename docs/{ => source}/Makefile (100%) rename docs/{ => source}/Tutorial/notebook.ipynb (100%) rename docs/{ => source}/conf.py (98%) rename docs/{ => source}/genindex.rst (100%) rename docs/{ => source}/images/aknowledge_logo.png (100%) rename docs/{ => source}/images/aknowledge_logo_negative.png (100%) rename docs/{ => source}/images/erc_fwf_logos.jpg (100%) rename docs/{ => source}/images/score_example.png (100%) rename docs/{ => source}/images/score_example_1.png (100%) rename docs/{ => source}/images/score_example_2.png (100%) rename docs/{ => source}/index.rst (100%) rename docs/{ => source}/introduction.rst (100%) rename docs/{ => source}/make.bat (100%) rename docs/{ => source}/modules/partitura.musicanalysis.rst (100%) rename docs/{ => source}/modules/partitura.performance.rst (100%) rename docs/{ => source}/modules/partitura.rst (100%) rename docs/{ => source}/modules/partitura.score.rst (100%) rename docs/{ => source}/modules/partitura.utils.rst (100%) diff --git a/README.md b/README.md index 5367181e..8c0f2621 100644 --- a/README.md +++ b/README.md @@ -241,8 +241,8 @@ agreement No. 670035 project ["Con Espressione"](https://www.jku.at/en/institute and the Austrian Science Fund (FWF) under grant P 29840-G26 (project ["Computer-assisted Analysis of Herbert von Karajan's Musical Conducting Style"](https://karajan-research.org/programs/musical-interpretation-karajan))

- - + +

[//]: # () diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..9fb4fbd6 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +partitura +sphinx>=5 +nbsphinx diff --git a/docs/Makefile b/docs/source/Makefile similarity index 100% rename from docs/Makefile rename to docs/source/Makefile diff --git a/docs/Tutorial/notebook.ipynb b/docs/source/Tutorial/notebook.ipynb similarity index 100% rename from docs/Tutorial/notebook.ipynb rename to docs/source/Tutorial/notebook.ipynb diff --git a/docs/conf.py b/docs/source/conf.py similarity index 98% rename from docs/conf.py rename to docs/source/conf.py index 51fa874a..1f43c771 100644 --- a/docs/conf.py +++ b/docs/source/conf.py @@ -14,7 +14,7 @@ import sys import pkg_resources -sys.path.insert(0, os.path.abspath("../partitura")) +sys.path.insert(0, os.path.abspath("../../partitura")) # The master toctree document. master_doc = "index" diff --git a/docs/genindex.rst b/docs/source/genindex.rst similarity index 100% rename from docs/genindex.rst rename to docs/source/genindex.rst diff --git a/docs/images/aknowledge_logo.png b/docs/source/images/aknowledge_logo.png similarity index 100% rename from docs/images/aknowledge_logo.png rename to docs/source/images/aknowledge_logo.png diff --git a/docs/images/aknowledge_logo_negative.png b/docs/source/images/aknowledge_logo_negative.png similarity index 100% rename from docs/images/aknowledge_logo_negative.png rename to docs/source/images/aknowledge_logo_negative.png diff --git a/docs/images/erc_fwf_logos.jpg b/docs/source/images/erc_fwf_logos.jpg similarity index 100% rename from docs/images/erc_fwf_logos.jpg rename to docs/source/images/erc_fwf_logos.jpg diff --git a/docs/images/score_example.png b/docs/source/images/score_example.png similarity index 100% rename from docs/images/score_example.png rename to docs/source/images/score_example.png diff --git a/docs/images/score_example_1.png b/docs/source/images/score_example_1.png similarity index 100% rename from docs/images/score_example_1.png rename to docs/source/images/score_example_1.png diff --git a/docs/images/score_example_2.png b/docs/source/images/score_example_2.png similarity index 100% rename from docs/images/score_example_2.png rename to docs/source/images/score_example_2.png diff --git a/docs/index.rst b/docs/source/index.rst similarity index 100% rename from docs/index.rst rename to docs/source/index.rst diff --git a/docs/introduction.rst b/docs/source/introduction.rst similarity index 100% rename from docs/introduction.rst rename to docs/source/introduction.rst diff --git a/docs/make.bat b/docs/source/make.bat similarity index 100% rename from docs/make.bat rename to docs/source/make.bat diff --git a/docs/modules/partitura.musicanalysis.rst b/docs/source/modules/partitura.musicanalysis.rst similarity index 100% rename from docs/modules/partitura.musicanalysis.rst rename to docs/source/modules/partitura.musicanalysis.rst diff --git a/docs/modules/partitura.performance.rst b/docs/source/modules/partitura.performance.rst similarity index 100% rename from docs/modules/partitura.performance.rst rename to docs/source/modules/partitura.performance.rst diff --git a/docs/modules/partitura.rst b/docs/source/modules/partitura.rst similarity index 100% rename from docs/modules/partitura.rst rename to docs/source/modules/partitura.rst diff --git a/docs/modules/partitura.score.rst b/docs/source/modules/partitura.score.rst similarity index 100% rename from docs/modules/partitura.score.rst rename to docs/source/modules/partitura.score.rst diff --git a/docs/modules/partitura.utils.rst b/docs/source/modules/partitura.utils.rst similarity index 100% rename from docs/modules/partitura.utils.rst rename to docs/source/modules/partitura.utils.rst diff --git a/requirements.txt b/requirements.txt index 10ee666f..d92da74e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,4 @@ scipy xmlschema lxml lark-parser -mido -nbsphinx \ No newline at end of file +mido \ No newline at end of file From 774c102e2aba6a2e4a8f90457ea64664c16e910e Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 7 Oct 2022 12:45:57 +0200 Subject: [PATCH 02/41] moved makefiles. --- docs/{source => }/Makefile | 0 docs/{source => }/make.bat | 70 +++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 35 deletions(-) rename docs/{source => }/Makefile (100%) rename docs/{source => }/make.bat (95%) diff --git a/docs/source/Makefile b/docs/Makefile similarity index 100% rename from docs/source/Makefile rename to docs/Makefile diff --git a/docs/source/make.bat b/docs/make.bat similarity index 95% rename from docs/source/make.bat rename to docs/make.bat index 2119f510..922152e9 100644 --- a/docs/source/make.bat +++ b/docs/make.bat @@ -1,35 +1,35 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd From 846767a2c266ccae8b5ff3e4da294650ead9dc0d Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 7 Oct 2022 12:47:15 +0200 Subject: [PATCH 03/41] update requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d92da74e..10ee666f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ scipy xmlschema lxml lark-parser -mido \ No newline at end of file +mido +nbsphinx \ No newline at end of file From 84b7b17f18bfaf0d0d6828e7a1f0ab73a8bd7abd Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 7 Oct 2022 13:49:21 +0200 Subject: [PATCH 04/41] added example.mei and example.krn to package data. --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index e02add30..d44fb762 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,8 @@ "assets/musicxml.xsd", "assets/score_example.mid", "assets/score_example.musicxml", + "assets/score_example.krn", + "assets/score_example.mei", ] }, install_requires=REQUIRED, From da1366a452975286e52c0de47bc97efab2275124 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 7 Oct 2022 13:57:10 +0200 Subject: [PATCH 05/41] updated make files. --- docs/Makefile | 4 ++-- docs/make.bat | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index d4bb2cbb..d0c3cbf1 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -5,8 +5,8 @@ # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build +SOURCEDIR = source +BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: diff --git a/docs/make.bat b/docs/make.bat index 922152e9..9534b018 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -7,8 +7,8 @@ REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) -set SOURCEDIR=. -set BUILDDIR=_build +set SOURCEDIR=source +set BUILDDIR=build if "%1" == "" goto help From baa1c860ce357aba2b5d6582cc4426d7daeb45a1 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 7 Oct 2022 13:58:07 +0200 Subject: [PATCH 06/41] read the docs yaml. --- .readthedocs.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000..1ccf124a --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,21 @@ + +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-20.04 + tools: + python: "3.8" + +#mkdocs: +# configuration: mkdocs.yml + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt \ No newline at end of file From 52bd75f374240038ad62c1afe1c68c4f07fa3166 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 7 Oct 2022 13:59:36 +0200 Subject: [PATCH 07/41] api update. --- docs/source/api.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 docs/source/api.rst diff --git a/docs/source/api.rst b/docs/source/api.rst new file mode 100644 index 00000000..cdb6fb69 --- /dev/null +++ b/docs/source/api.rst @@ -0,0 +1,10 @@ +API +=== + +.. autosummary:: + :toctree: generated + + partitura.score + partitura.performance + partitura.utils + partitura.musicanalysis From 8ec0c2504d322943ae6255a1f36dc6d3422d71f3 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 7 Oct 2022 14:05:49 +0200 Subject: [PATCH 08/41] Added API.rst to index --- docs/source/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index 56700fbf..ce4b397a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,6 +19,7 @@ Partitura documentation :maxdepth: 2 :caption: API Reference + api ./modules/partitura ./modules/partitura.score ./modules/partitura.performance From 90dfdf0fa182400eaefaae6cf203d643aa86b3e2 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 7 Oct 2022 15:16:18 +0200 Subject: [PATCH 09/41] update of api. --- docs/source/index.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index ce4b397a..0432aa6f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -20,10 +20,5 @@ Partitura documentation :caption: API Reference api - ./modules/partitura - ./modules/partitura.score - ./modules/partitura.performance - ./modules/partitura.musicanalysis - ./modules/partitura.utils From 668a51f7e098549ec9a40ff65ba470a027899cd6 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 7 Oct 2022 15:51:31 +0200 Subject: [PATCH 10/41] removed api file --- docs/source/api.rst | 10 ---------- docs/source/index.rst | 6 +++++- 2 files changed, 5 insertions(+), 11 deletions(-) delete mode 100644 docs/source/api.rst diff --git a/docs/source/api.rst b/docs/source/api.rst deleted file mode 100644 index cdb6fb69..00000000 --- a/docs/source/api.rst +++ /dev/null @@ -1,10 +0,0 @@ -API -=== - -.. autosummary:: - :toctree: generated - - partitura.score - partitura.performance - partitura.utils - partitura.musicanalysis diff --git a/docs/source/index.rst b/docs/source/index.rst index 0432aa6f..56700fbf 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,6 +19,10 @@ Partitura documentation :maxdepth: 2 :caption: API Reference - api + ./modules/partitura + ./modules/partitura.score + ./modules/partitura.performance + ./modules/partitura.musicanalysis + ./modules/partitura.utils From 16020c65dd0f9e500c6adc384824dafabdd20895 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 10 Oct 2022 15:17:56 +0200 Subject: [PATCH 11/41] removed local outputs and warnings. --- docs/source/Tutorial/notebook.ipynb | 72 +++++++---------------------- 1 file changed, 17 insertions(+), 55 deletions(-) diff --git a/docs/source/Tutorial/notebook.ipynb b/docs/source/Tutorial/notebook.ipynb index f8aa74ca..1485e0ff 100644 --- a/docs/source/Tutorial/notebook.ipynb +++ b/docs/source/Tutorial/notebook.ipynb @@ -39,7 +39,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "facial-quarterly", "metadata": { "colab": { @@ -47,25 +47,12 @@ }, "id": "PeabdL1k7YC4", "outputId": "fcb7d1be-27a1-4c79-c5d3-8cbfa54cae44", - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: partitura in /home/manos/Desktop/JKU/codes/partitura (1.0.0)\r\n", - "Requirement already satisfied: numpy in /home/manos/miniconda3/envs/partitura/lib/python3.8/site-packages (from partitura) (1.21.2)\r\n", - "Requirement already satisfied: scipy in /home/manos/miniconda3/envs/partitura/lib/python3.8/site-packages (from partitura) (1.7.1)\r\n", - "Requirement already satisfied: lxml in /home/manos/miniconda3/envs/partitura/lib/python3.8/site-packages (from partitura) (4.6.3)\r\n", - "Requirement already satisfied: lark-parser in /home/manos/miniconda3/envs/partitura/lib/python3.8/site-packages (from partitura) (0.12.0)\r\n", - "Requirement already satisfied: xmlschema in /home/manos/miniconda3/envs/partitura/lib/python3.8/site-packages (from partitura) (1.8.0)\r\n", - "Requirement already satisfied: mido in /home/manos/miniconda3/envs/partitura/lib/python3.8/site-packages (from partitura) (1.2.10)\r\n", - "Requirement already satisfied: elementpath<3.0.0,>=2.2.2 in /home/manos/miniconda3/envs/partitura/lib/python3.8/site-packages (from xmlschema->partitura) (2.3.2)\r\n", - "fatal: destination path 'partitura_tutorial' already exists and is not an empty directory.\r\n" - ] + "scrolled": true, + "pycharm": { + "is_executing": true } - ], + }, + "outputs": [], "source": [ "# Install partitura\n", "! pip install partitura\n", @@ -73,7 +60,8 @@ "# To be able to access helper modules in the repo for this tutorial\n", "# (not necessary if the jupyter notebook is run locally instead of google colab)\n", "!git clone https://github.com/CPJKU/partitura_tutorial.git\n", - " \n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", "import sys, os\n", "sys.path.insert(0, os.path.join(os.getcwd(), \"partitura_tutorial\", \"content\"))\n", "sys.path.insert(0,'/content/partitura_tutorial/content')\n" @@ -122,7 +110,7 @@ "application/vnd.jupyter.widget-view+json": { "version_major": 2, "version_minor": 0, - "model_id": "6918ecbb7839408cb384594317cddb27" + "model_id": "51b999065d4e4460b960ff64e7507006" } }, "metadata": {}, @@ -319,7 +307,7 @@ "outputs": [ { "data": { - "text/plain": "[,\n ,\n ]" + "text/plain": "[,\n ,\n ]" }, "execution_count": 5, "metadata": {}, @@ -839,16 +827,7 @@ "execution_count": 26, "id": "passing-lending", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/manos/Desktop/JKU/codes/partitura/partitura/io/importmidi.py:128: UserWarning: change of Tempo to mpq = 500000 and resulting seconds per tick = 0.000125at time: 0.0\n", - " warnings.warn(\n" - ] - } - ], + "outputs": [], "source": [ "# Note array from a performance\n", "\n", @@ -912,11 +891,11 @@ "name": "stdout", "output_type": "stream", "text": [ - "[(5.6075 , 5.5025 , 72, 37, 1, 0, 'n0')\n", - " (5.63375, 5.47625, 60, 27, 1, 0, 'n1')\n", - " (6.07 , 5.04 , 72, 45, 1, 0, 'n2')\n", - " (6.11125, 4.99875, 60, 26, 1, 0, 'n3')\n", - " (6.82625, 4.28375, 60, 39, 1, 0, 'n4')]\n" + "[(5.6075 , 5.5025 , 72, 37, 0, 0, 'n0')\n", + " (5.63375, 5.47625, 60, 27, 0, 0, 'n1')\n", + " (6.07 , 5.04 , 72, 45, 0, 0, 'n2')\n", + " (6.11125, 4.99875, 60, 26, 0, 0, 'n3')\n", + " (6.82625, 4.28375, 60, 39, 0, 0, 'n4')]\n" ] } ], @@ -1089,14 +1068,6 @@ "text": [ "[(0.25, 47, 1) (1.25, 47, 1) (2.25, 47, 1) (3. , 68, 1) (3.25, 47, 1)]\n" ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/manos/Desktop/JKU/codes/partitura/partitura/directions.py:533: UserWarning: error parsing \"ritenuto\" (UnexpectedCharacters)\n", - " warnings.warn('error parsing \"{}\" ({})'.format(string, type(e).__name__))\n" - ] } ], "source": [ @@ -1354,16 +1325,7 @@ "execution_count": 40, "id": "rolled-cloud", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/tmp/ipykernel_907569/209301002.py:4: DeprecationWarning: `create_part` is deprecated as an argument to `load_match`; use `create_score` instead.\n", - " performed_part, alignment, score_part = pt.load_match(match_fn, create_part=True)\n" - ] - } - ], + "outputs": [], "source": [ "# path to the match\n", "match_fn = os.path.join(MATCH_DIR, 'Chopin_op10_no3_p01.match')\n", From 4ed5f94cc81b587615fed7115da2b028096d4417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Eduardo=20Cancino-Chac=C3=B3n?= Date: Mon, 10 Oct 2022 16:16:22 +0200 Subject: [PATCH 12/41] minor typo (capitalization) github -> GitHub --- docs/source/Tutorial/notebook.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/Tutorial/notebook.ipynb b/docs/source/Tutorial/notebook.ipynb index 1485e0ff..fc75b60c 100644 --- a/docs/source/Tutorial/notebook.ipynb +++ b/docs/source/Tutorial/notebook.ipynb @@ -1529,7 +1529,7 @@ "\n", "Thank you for trying out partitura! We hope it serves you well. \n", "\n", - "If you miss a particular functionality or encounter a bug, we appreciate it if you raise an issue on github: https://github.com/CPJKU/partitura/issues" + "If you miss a particular functionality or encounter a bug, we appreciate it if you raise an issue on GitHub: https://github.com/CPJKU/partitura/issues" ] } ], From 03157067119c41635408eb7ecca3354967981973 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 10 Oct 2022 17:28:49 +0200 Subject: [PATCH 13/41] Correction on voice assignment. --- partitura/utils/music.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 31733263..de197ef3 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -2297,7 +2297,10 @@ def note_array_from_note_list( # Sanitize voice information no_voice_idx = np.where(note_array["voice"] == -1)[0] - max_voice = note_array["voice"].max() + try: + max_voice = note_array["voice"].max() + except ValueError: # raised if `y` is empty. + max_voice = 0 note_array["voice"][no_voice_idx] = max_voice + 1 # sort by onset and pitch From 7c6f02775d78769d42b0e05366ff6f4f09a76164 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 10 Oct 2022 17:28:59 +0200 Subject: [PATCH 14/41] Fix for zero divs. --- partitura/io/importkern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index 2c4983a6..f10b546e 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -516,7 +516,7 @@ def find_lcm(self, doc): match = re.findall(r"([0-9]+)([a-g]|[A-G]|r|\.)", kern_string) durs, _ = zip(*match) x = np.array(list(map(lambda x: int(x), durs))) - divs = np.lcm.reduce(np.unique(x)) + divs = np.lcm.reduce(np.unique(x[x != 0])) return float(divs) / 4.00 From 5e3d4ce4709268356929967cc7634b579a966a12 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 10 Oct 2022 17:51:37 +0200 Subject: [PATCH 15/41] Fix for empty voice. --- partitura/utils/music.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index de197ef3..6dd11b30 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -2299,7 +2299,8 @@ def note_array_from_note_list( no_voice_idx = np.where(note_array["voice"] == -1)[0] try: max_voice = note_array["voice"].max() - except ValueError: # raised if `y` is empty. + except ValueError: # raised if `note_array["voice"]` is empty. + note_array["voice"] = 0 max_voice = 0 note_array["voice"][no_voice_idx] = max_voice + 1 From 8fb966f9169c7a8b549fca55cfe345ac0a17880f Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Mon, 10 Oct 2022 18:10:22 +0200 Subject: [PATCH 16/41] fix for ValueError: max() arg is an empty sequence --- partitura/score.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/partitura/score.py b/partitura/score.py index 9b3bb49c..18548291 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -4587,12 +4587,12 @@ def merge_parts(parts, reassign="voice"): note_arrays = [part.note_array(include_staff=True) for part in parts] # find the maximum number of voices for each part (voice number start from 1) maximum_voices = [ - max(note_array["voice"]) if max(note_array["voice"]) != 0 else 1 + max(note_array["voice"], default=0) if max(note_array["voice"], default=0) != 0 else 1 for note_array in note_arrays ] # find the maximum number of staves for each part (staff number start from 0 but we force them to 1) maximum_staves = [ - max(note_array["staff"]) if max(note_array["staff"]) != 0 else 1 + max(note_array["staff"], default=0) if max(note_array["staff"], default=0) != 0 else 1 for note_array in note_arrays ] From d26e45e4f67d4630d7d510b876a7bb5be15575cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Tue, 11 Oct 2022 09:53:54 +0200 Subject: [PATCH 17/41] allow for using callables for specifiying tuning for synthesis --- partitura/io/exportaudio.py | 28 +++++++++++++++++++++------- partitura/utils/synth.py | 9 ++++++++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/partitura/io/exportaudio.py b/partitura/io/exportaudio.py index 61ede0ca..686cf5f0 100644 --- a/partitura/io/exportaudio.py +++ b/partitura/io/exportaudio.py @@ -1,14 +1,14 @@ """ Synthesize Partitura object to wav using additive synthesis """ -from typing import Union, Optional +from typing import Union, Optional, Callable, Dict, Any import numpy as np from scipy.io import wavfile from partitura.score import ScoreLike from partitura.performance import PerformanceLike -from partitura.utils.synth import synthesize, SAMPLE_RATE +from partitura.utils.synth import synthesize, SAMPLE_RATE, A4 from partitura.utils.misc import PathLike @@ -18,9 +18,10 @@ def save_wav( input_data: Union[ScoreLike, PerformanceLike, np.ndarray], out: Optional[PathLike] = None, - samplerate=SAMPLE_RATE, - envelope_fun="linear", - tuning="equal_temperament", + samplerate: int = SAMPLE_RATE, + envelope_fun: Union[str, Callable] = "linear", + tuning: Union[str, Callable] = "equal_temperament", + tuning_kwargs: Dict[str, Any] = {"a4": A4}, harmonic_dist: Optional[Union[str, int]] = None, bpm: Union[float, int] = 60, ) -> Optional[np.ndarray]: @@ -39,9 +40,21 @@ def save_wav( the audio signal as an array (see `audio_signal` below). samplerate: int The sample rate of the audio file in Hz. The default is 44100Hz. - envelope_fun: {"linear", "exp" } + envelope_fun: {"linear", "exp" } or callable The type of envelop to apply to the individual sine waves. - tuning: {"equal_temperament", "natural"} + If "linear" or "exp", the methods `lin_in_lin_out` and `exp_in_exp_out` + in `partitura.utils.synth` will be used. Otherwise this argument should + be a callable. See `lin_in_lin_out` for more details. + tuning: {"equal_temperament", "natural"} or callable + Method for tuning. If "equal temperament" will use equally spaced + semitones, while "natural" will use natural ratios within an octave. + Otherwise it uses a callable. See `midi_pitch_to_natural_frequency` + and `midi_pitch_to_frequency` for more info on the "equal_temperament" + and "natural" tuning. + tuning_kwargs : dict + Keyword arguments to be passed to the tuning method selected in `tuning`. See + `midi_pitch_to_natural_frequency` and `midi_pitch_to_frequency` + for more info on the "equal_temperament" and "natural" tuning. harmonic_dist : int, "shepard" or None (optional) Distribution of harmonics. If an integer, it is the number of harmonics to be considered. If "shepard", it uses Shepard tones. @@ -60,6 +73,7 @@ def save_wav( samplerate=samplerate, envelope_fun=envelope_fun, tuning=tuning, + tuning_kwargs=tuning_kwargs, harmonic_dist=harmonic_dist, bpm=bpm, ) diff --git a/partitura/utils/synth.py b/partitura/utils/synth.py index ae264d22..cb1a80d2 100644 --- a/partitura/utils/synth.py +++ b/partitura/utils/synth.py @@ -273,7 +273,7 @@ def synthesize( note_info, samplerate: int = SAMPLE_RATE, envelope_fun: str = "linear", - tuning: str = "equal_temperament", + tuning: Union[str, Callable] = "equal_temperament", tuning_kwargs: Dict[str, Any] = {"a4": A4}, harmonic_dist: Optional[Union[str, int]] = None, bpm: Union[float, int] = 60, @@ -344,6 +344,13 @@ def synthesize( freq_in_hz = midi_pitch_to_frequency(pitch, **tuning_kwargs) elif tuning == "natural": freq_in_hz = midi_pitch_to_natural_frequency(pitch, **tuning_kwargs) + elif callable(tuning): + freq_in_hz = tuning(pitch, **tuning_kwargs) + + else: + raise ValueError( + "`tuning` must be 'equal_temperament', 'natural' or a callable" + ) if harmonic_dist is None: From d5fa5f95bc2322759a7d18077aeb835186ec8298 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 11 Oct 2022 16:29:48 +0200 Subject: [PATCH 18/41] fix for inferring repeats without ending. --- partitura/io/importmusicxml.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index df875b40..641d3ee8 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -347,6 +347,26 @@ def _parse_parts(document, part_dict): "Single measure bracket is assumed" ) + # Complete repeats without end. + volta_repeats = list() + for o in part.iter_all(score.Repeat, mode="starting"): + if o.end is None: + # if len(o.start.starting_objects[score.Repeat]) > 0: + # starting = list(o.start.starting_objects[score.Repeat].keys())[0] + # # if unstarted repeat from volta, continue for now + # if len(starting.end.ending_objects[score.Repeat]) > 0: + # # if repeat from volta, continue for now + # volta_repeats.append(o) + # continue + + end_times = [r.start.t for r in part.iter_all(score.Repeat)] + [part._points[-1].t] + end_time_id = np.searchsorted(end_times, o.start.t+1) + part.add(o, None, end_times[end_time_id]) + warnings.warn( + "Found repeat without end\n" + "Ending point {} is assumend".format(end_times[end_time_id]) + ) + # complete unstarted repeats volta_repeats = list() for o in part.iter_all(score.Repeat, mode="ending"): From 35ee576dce26736eee2283f9eb9777b9377411d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Thu, 13 Oct 2022 17:47:55 +0200 Subject: [PATCH 19/41] add parameters (wip) --- partitura/utils/music.py | 105 +++++++++++++++++++++++++++++++++++---- tests/test_synth.py | 28 ++++++++++- 2 files changed, 121 insertions(+), 12 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 31733263..73e23112 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -413,7 +413,7 @@ def pitch_spelling_to_note_name(step, alter, octave): def midi_pitch_to_frequency( - midi_pitch: Union[int, float, np.ndarray], a4: Union[int, float] = A4 + midi_pitch: Union[int, float, np.ndarray], a4: Union[int, float] = A4 ) -> Union[float, np.ndarray]: """ Convert MIDI pitch to frequency in Hz. This method assumes equal temperament. @@ -435,8 +435,8 @@ def midi_pitch_to_frequency( def frequency_to_midi_pitch( - freq: Union[int, float, np.ndarray], - a4: Union[int, float] = A4, + freq: Union[int, float, np.ndarray], + a4: Union[int, float] = A4, ) -> Union[int, np.ndarray]: """ Convert frequency to MIDI pitch. This method assumes equal temperament. @@ -1601,15 +1601,10 @@ def note_array_from_part_list( kwargs["include_divs_per_quarter"] = True is_score = True if isinstance(part, Part): - na = note_array_from_part( - part, - **kwargs - ) + na = note_array_from_part(part, **kwargs) elif isinstance(part, PartGroup): na = note_array_from_part_list( - part.children, - unique_id_per_part=unique_id_per_part, - **kwargs + part.children, unique_id_per_part=unique_id_per_part, **kwargs ) elif isinstance(part, PerformedPart): na = part.note_array() @@ -2621,6 +2616,96 @@ def performance_from_part(part, bpm=100, velocity=64): return ppart +def performance_notearray_from_score_notearray( + snote_array: np.ndarray, + bpm: float = 100, + velocity: Union[np.ndarray, int] = 64, +) -> np.ndarray: + """ + Generate a performance note array from a score note array + + Parameters + ---------- + snote_array : np.ndarray + A score note array. + bpm : float + Beats per minute to generate the performance. Default is 100. + velocity: int or np.ndarray + The MIDI velocity for all notes. If a numpy array is given, it should + be the same length as `snote_array`. + + Returns + ------- + pnote_array : np.ndarray + """ + + ppart_fields = [ + ("onset_sec", "f4"), + ("duration_sec", "f4"), + ("pitch", "i4"), + ("velocity", "i4"), + ("track", "i4"), + ("channel", "i4"), + ("id", "U256"), + ] + + pnote_array = np.zeros(len(snote_array), dtype=ppart_fields) + + if isinstance(velocity, np.ndarray): + if len(velocity) != len(snote_array): + raise ValueError( + "The provided MIDI velocity should be an integer or have " + "the same length as `snote_array`." + ) + pnote_array["velocity"] = np.round(velocity).astype(int) + + elif callable(velocity): + # The velocity parameter is a callable that returns a + # velocity value for each score onset + pnote_array["velocity"] = np.round( + velocity(pnote_array["onset_sec"]), + ).astype(int) + + else: + pnote_array["velocity"] = int(velocity) + + unique_onsets = np.unique(snote_array["onset_beat"]) + # Cast as object to avoid warnings, but seems to work well + # in numpy version 1.20.1 + unique_onset_idxs = np.array( + [np.where(snote_array["onset_beat"] == u)[0] for u in unique_onsets], + dtype=object, + ) + + iois = np.diff(unique_onsets) + + if callable(bpm): + # bpm parameter is a callable that returns a bpm value + # for each score onset + bp = 60 / bpm(unique_onsets) + elif isinstance(bpm, np.ndarray): + if len(bpm) != len(unique_onsets) or bpm.ndim != 2: + raise ValueError( + f"`bpm` should be a 2D array with length {len(unique_onsets)}" + f" but has length {len(bpm}}" + ) + # convert bpm to beat period + bp = 60 / float(bpm) + + # TODO: allow for variable bpm and velocity + pnote_array["duration_sec"] = bp * snote_array["duration_beat"] + pnote_array["pitch"] = snote_array["pitch"] + pnote_array["id"] = snote_array["id"] + p_onsets = np.r_[0, np.cumsum(iois * bp)] + + for ix, on in zip(unique_onset_idxs, p_onsets): + # ix has to be cast as integer depending on the + # numpy version... + pnote_array["onset_sec"][ix.astype(int)] = on + + return pnote_array + + def get_time_maps_from_alignment( ppart_or_note_array, spart_or_note_array, alignment, remove_ornaments=True ): diff --git a/tests/test_synth.py b/tests/test_synth.py index ace725ee..4fa00fb3 100644 --- a/tests/test_synth.py +++ b/tests/test_synth.py @@ -16,10 +16,10 @@ from partitura import save_wav -RNG = np.random.RandomState(1984) - from tests import WAV_TESTFILES +RNG = np.random.RandomState(1984) + class TestMidiPitchToNaturalFrequency(unittest.TestCase): def test_octaves(self): @@ -158,6 +158,30 @@ def test_export(self): self.assertTrue(sr_rec == sr) self.assertTrue(len(rec_audio) == len(original_audio)) + self.assertTrue( + np.allclose( + rec_audio / rec_audio.max(), + original_audio / original_audio.max(), + atol=1e-4, + ) + ) + + def test_errors(self): + + # wrong envelope + try: + audio_signal = synthesize( + note_info=self.score, + samplerate=8000, + envelope_fun="wrong keyword", + tuning="equal_temperament", + bpm=60, + ) + # This test should fail + self.assertTrue(False) + except ValueError: + self.assertTrue(True) + if __name__ == "__main__": unittest.main() From aac0fabfcc0df82c9374fa7d556520d255b72589 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 14 Oct 2022 17:53:16 +0200 Subject: [PATCH 20/41] fix computing note_array from score when parts without notes exist. --- partitura/utils/music.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 6dd11b30..c16adb64 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -1622,7 +1622,7 @@ def note_array_from_part_list( if is_score: # rescale if parts have different divs - divs_per_parts = [part[0]["divs_pq"] for part in note_array] + divs_per_parts = [part_na[0]["divs_pq"] for part_na in note_array if len(part_na)] lcm = np.lcm.reduce(divs_per_parts) time_multiplier_per_part = [int(lcm / d) for d in divs_per_parts] for na, time_mult in zip(note_array, time_multiplier_per_part): From 32787a34f450b167ff923596ae56fe07ebfb7930 Mon Sep 17 00:00:00 2001 From: sildater <41552783+sildater@users.noreply.github.com> Date: Wed, 19 Oct 2022 12:10:44 +0200 Subject: [PATCH 21/41] add reference-based midi pitch to freq --- partitura/utils/synth.py | 55 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/partitura/utils/synth.py b/partitura/utils/synth.py index ae264d22..0d53765a 100644 --- a/partitura/utils/synth.py +++ b/partitura/utils/synth.py @@ -71,12 +71,8 @@ def midi_pitch_to_natural_frequency( C4 is a descending major sixth with respect to A4, E5 is descending perfect fourth computed with respect to A5, etc.). - - TODO - ---- - * compute intervals with given reference pitch. """ - + octave = (midi_pitch // 12) - 1 aref = 69.0 - 12.0 * (4 - octave) @@ -102,6 +98,55 @@ def midi_pitch_to_natural_frequency( return freqs +def midi_pitch_to_tempered_frequency( + midi_pitch: Union[int, float, np.ndarray], + reference_midi_pitch: Union[int, float] = 69, + reference_frequency: float = A4, + interval_ratios: Dict[int, float] = NATURAL_INTERVAL_RATIOS, +) -> Union[float, np.ndarray]: + """ + Convert MIDI pitch to frequency in Hz using + a temperament given as frequency ratios above + a reference pitch. + + Parameters + ---------- + midi_pitch: int, float or ndarray + MIDI pitch of the note(s). + reference_midi_pitch : int or float (optional) + midi pitch of the reference pitch. By default is 69 (A4). + reference_frequency : int (optional) + Frequency of A4 in Hz. By default is 440 Hz. + interval_ratios: dict + Dictionary of interval ratios from the reference + + Returns + ------- + freq : float or ndarray + Frequency of the note(s). + """ + + interval = (midi_pitch - reference_midi_pitch) % 12 + octave = (midi_pitch - reference_midi_pitch) // 12 + adjusted_reference_frequency = reference_frequency / (2.0 ** -octave) + + if isinstance(interval, (int, float)): + interval = np.array([interval], dtype=int) + + ratios = np.array( + [ + interval_ratios[abs(itv)] + for itv in interval + ] + ) + + freqs = adjusted_reference_frequency * ratios + + if isinstance(midi_pitch, (int, float)): + freqs = float(freqs) + return freqs + + def exp_in_exp_out( num_frames: int, dtype: type = DTYPE, From 005aba513427c7ea423a9cfc6a57c2ba55784e30 Mon Sep 17 00:00:00 2001 From: sildater <41552783+sildater@users.noreply.github.com> Date: Wed, 19 Oct 2022 18:14:12 +0200 Subject: [PATCH 22/41] starting repeats safety --- partitura/io/importmusicxml.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index 641d3ee8..b5c44b9e 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -358,8 +358,9 @@ def _parse_parts(document, part_dict): # # if repeat from volta, continue for now # volta_repeats.append(o) # continue - - end_times = [r.start.t for r in part.iter_all(score.Repeat)] + [part._points[-1].t] + + starting_repeats = [r for r in part.iter_all(score.Repeat) if r.start is not None] + end_times = [r.start.t for r in starting_repeats] + [part._points[-1].t] end_time_id = np.searchsorted(end_times, o.start.t+1) part.add(o, None, end_times[end_time_id]) warnings.warn( From a4830465fe088ae3c695c49d22474c1e1896bbb3 Mon Sep 17 00:00:00 2001 From: sildater <41552783+sildater@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:33:01 +0200 Subject: [PATCH 23/41] enable divs from perf midi --- partitura/io/importmidi.py | 15 +++++++++++---- partitura/performance.py | 2 ++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/partitura/io/importmidi.py b/partitura/io/importmidi.py index ea9778b2..190a03ec 100644 --- a/partitura/io/importmidi.py +++ b/partitura/io/importmidi.py @@ -68,6 +68,7 @@ def load_performance_midi( filename: Union[PathLike, mido.MidiFile], default_bpm: Union[int, float] = 120, merge_tracks: bool = False, + time_in_divs: bool = False, ) -> performance.Performance: """Load a musical performance from a MIDI file. @@ -111,7 +112,10 @@ def load_performance_midi( mpq = 60 * (10 ** 6 / default_bpm) # convert MIDI ticks in seconds - time_conversion_factor = mpq / (ppq * 10 ** 6) + if time_in_divs: + time_conversion_factor = 1 + else: + time_conversion_factor = mpq / (ppq * 10 ** 6) notes = [] controls = [] @@ -134,8 +138,11 @@ def load_performance_midi( if msg.type == "set_tempo": mpq = msg.tempo - - time_conversion_factor = mpq / (ppq * 10 ** 6) + + if time_in_divs: + time_conversion_factor = 1 + else: + time_conversion_factor = mpq / (ppq * 10 ** 6) warnings.warn( ( @@ -225,7 +232,7 @@ def load_performance_midi( for i, note in enumerate(notes): note["id"] = f"n{i}" - pp = performance.PerformedPart(notes, controls=controls, programs=programs) + pp = performance.PerformedPart(notes, controls=controls, programs=programs, ppq = ppq) perf = performance.Performance( id=doc_name, diff --git a/partitura/performance.py b/partitura/performance.py index c0a6c1a9..4af5e822 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -73,6 +73,7 @@ def __init__( controls: List[dict] = None, programs: List[dict] = None, sustain_pedal_threshold: int = 64, + ppq: int = 500 ) -> None: super().__init__() self.id = id @@ -80,6 +81,7 @@ def __init__( self.notes = notes self.controls = controls or [] self.programs = programs or [] + self.ppq = ppq self.sustain_pedal_threshold = sustain_pedal_threshold From 6c476aa2e739ec4d82522d6d3a93a4c71b3d216b Mon Sep 17 00:00:00 2001 From: sildater <41552783+sildater@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:38:44 +0200 Subject: [PATCH 24/41] document ppq --- partitura/performance.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/partitura/performance.py b/partitura/performance.py index 4af5e822..1818eac0 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -48,6 +48,8 @@ class PerformedPart(object): The threshold above which sustain pedal values are considered to be equivalent to on. For values below the threshold the sustain pedal is treated as off. Defaults to 64. + ppq : int + Parts per Quarter (ppq) of the MIDI encoding. Defaults to 480. Attributes ---------- @@ -73,7 +75,7 @@ def __init__( controls: List[dict] = None, programs: List[dict] = None, sustain_pedal_threshold: int = 64, - ppq: int = 500 + ppq: int = 480 ) -> None: super().__init__() self.id = id From fad5298801cd1a19b62b522ea5a61f39d03991fb Mon Sep 17 00:00:00 2001 From: sildater <41552783+sildater@users.noreply.github.com> Date: Thu, 20 Oct 2022 09:09:27 +0200 Subject: [PATCH 25/41] add test for tempered frequency --- tests/test_synth.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_synth.py b/tests/test_synth.py index ace725ee..eb2bf152 100644 --- a/tests/test_synth.py +++ b/tests/test_synth.py @@ -6,6 +6,7 @@ from partitura.utils.synth import ( midi_pitch_to_natural_frequency, + midi_pitch_to_tempered_frequency, exp_in_exp_out, lin_in_lin_out, additive_synthesis, @@ -21,6 +22,21 @@ from tests import WAV_TESTFILES +class TestMidiPitchToTemperedFrequency(unittest.TestCase): + def test_octaves(self): + # all As + midi_pitch = np.arange(6) + 10 + freq_ratios = [1.1, 1.2, 1.3, 1.44, 1.5, 1.55] + # compute frequencies + frequency = midi_pitch_to_tempered_frequency(midi_pitch, + reference_midi_pitch = 10, + reference_frequency = 100, + interval_ratios = freq_ratios) + freq_ratios_new = frequency / 100 + # make test + self.assertTrue(np.allclose(freq_ratios, freq_ratios_new)) + + class TestMidiPitchToNaturalFrequency(unittest.TestCase): def test_octaves(self): # all As From b4fa182ca2bb037623c438768efc55281a4acbdd Mon Sep 17 00:00:00 2001 From: sildater <41552783+sildater@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:33:01 +0200 Subject: [PATCH 26/41] enable divs from perf midi --- partitura/io/importmidi.py | 15 +++++++++++---- partitura/performance.py | 2 ++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/partitura/io/importmidi.py b/partitura/io/importmidi.py index ea9778b2..190a03ec 100644 --- a/partitura/io/importmidi.py +++ b/partitura/io/importmidi.py @@ -68,6 +68,7 @@ def load_performance_midi( filename: Union[PathLike, mido.MidiFile], default_bpm: Union[int, float] = 120, merge_tracks: bool = False, + time_in_divs: bool = False, ) -> performance.Performance: """Load a musical performance from a MIDI file. @@ -111,7 +112,10 @@ def load_performance_midi( mpq = 60 * (10 ** 6 / default_bpm) # convert MIDI ticks in seconds - time_conversion_factor = mpq / (ppq * 10 ** 6) + if time_in_divs: + time_conversion_factor = 1 + else: + time_conversion_factor = mpq / (ppq * 10 ** 6) notes = [] controls = [] @@ -134,8 +138,11 @@ def load_performance_midi( if msg.type == "set_tempo": mpq = msg.tempo - - time_conversion_factor = mpq / (ppq * 10 ** 6) + + if time_in_divs: + time_conversion_factor = 1 + else: + time_conversion_factor = mpq / (ppq * 10 ** 6) warnings.warn( ( @@ -225,7 +232,7 @@ def load_performance_midi( for i, note in enumerate(notes): note["id"] = f"n{i}" - pp = performance.PerformedPart(notes, controls=controls, programs=programs) + pp = performance.PerformedPart(notes, controls=controls, programs=programs, ppq = ppq) perf = performance.Performance( id=doc_name, diff --git a/partitura/performance.py b/partitura/performance.py index c0a6c1a9..4af5e822 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -73,6 +73,7 @@ def __init__( controls: List[dict] = None, programs: List[dict] = None, sustain_pedal_threshold: int = 64, + ppq: int = 500 ) -> None: super().__init__() self.id = id @@ -80,6 +81,7 @@ def __init__( self.notes = notes self.controls = controls or [] self.programs = programs or [] + self.ppq = ppq self.sustain_pedal_threshold = sustain_pedal_threshold From 34d1a0e4c128eaf99783cf1648bef45e66e13951 Mon Sep 17 00:00:00 2001 From: sildater <41552783+sildater@users.noreply.github.com> Date: Wed, 19 Oct 2022 19:38:44 +0200 Subject: [PATCH 27/41] document ppq --- partitura/performance.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/partitura/performance.py b/partitura/performance.py index 4af5e822..1818eac0 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -48,6 +48,8 @@ class PerformedPart(object): The threshold above which sustain pedal values are considered to be equivalent to on. For values below the threshold the sustain pedal is treated as off. Defaults to 64. + ppq : int + Parts per Quarter (ppq) of the MIDI encoding. Defaults to 480. Attributes ---------- @@ -73,7 +75,7 @@ def __init__( controls: List[dict] = None, programs: List[dict] = None, sustain_pedal_threshold: int = 64, - ppq: int = 500 + ppq: int = 480 ) -> None: super().__init__() self.id = id From 8bb0f4c13e60265c80689d86be34d338941ce0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Fri, 21 Oct 2022 15:04:39 +0200 Subject: [PATCH 28/41] add tests for performance_notearray_from_score_notearray --- partitura/utils/music.py | 82 ++++++++++---- tests/test_utils.py | 229 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 286 insertions(+), 25 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 60ee61b2..1a78e7d5 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -5,7 +5,7 @@ import numpy as np from scipy.interpolate import interp1d from scipy.sparse import csc_matrix -from typing import Union +from typing import Union, Callable from partitura.utils.generic import find_nearest, search, iter_current_next MIDI_BASE_CLASS = {"c": 0, "d": 2, "e": 4, "f": 5, "g": 7, "a": 9, "b": 11} @@ -1617,7 +1617,9 @@ def note_array_from_part_list( if is_score: # rescale if parts have different divs - divs_per_parts = [part_na[0]["divs_pq"] for part_na in note_array if len(part_na)] + divs_per_parts = [ + part_na[0]["divs_pq"] for part_na in note_array if len(part_na) + ] lcm = np.lcm.reduce(divs_per_parts) time_multiplier_per_part = [int(lcm / d) for d in divs_per_parts] for na, time_mult in zip(note_array, time_multiplier_per_part): @@ -2622,8 +2624,8 @@ def performance_from_part(part, bpm=100, velocity=64): def performance_notearray_from_score_notearray( snote_array: np.ndarray, - bpm: float = 100, - velocity: Union[np.ndarray, int] = 64, + bpm: [float, np.ndarray, Callable] = 100.0, + velocity: Union[int, np.ndarray, Callable] = 64, ) -> np.ndarray: """ Generate a performance note array from a score note array @@ -2641,6 +2643,8 @@ def performance_notearray_from_score_notearray( Returns ------- pnote_array : np.ndarray + A performance note array based on the score with the specified tempo + and velocity. """ ppart_fields = [ @@ -2656,18 +2660,28 @@ def performance_notearray_from_score_notearray( pnote_array = np.zeros(len(snote_array), dtype=ppart_fields) if isinstance(velocity, np.ndarray): - if len(velocity) != len(snote_array): - raise ValueError( - "The provided MIDI velocity should be an integer or have " - "the same length as `snote_array`." + + if velocity.ndim == 2: + + velocity_fun = interp1d( + x=velocity[:, 0], + y=velocity[:, 1], + kind="linear", + bounds_error=False, + fill_value="interpolate", ) - pnote_array["velocity"] = np.round(velocity).astype(int) + pnote_array["velocity"] = np.round( + velocity_fun(snote_array["onset_beat"]), + ).astype(int) + + else: + pnote_array["velocity"] = np.round(velocity).astype(int) elif callable(velocity): # The velocity parameter is a callable that returns a # velocity value for each score onset pnote_array["velocity"] = np.round( - velocity(pnote_array["onset_sec"]), + velocity(snote_array["onset_beat"]), ).astype(int) else: @@ -2683,24 +2697,44 @@ def performance_notearray_from_score_notearray( iois = np.diff(unique_onsets) - if callable(bpm): - # bpm parameter is a callable that returns a bpm value - # for each score onset - bp = 60 / bpm(unique_onsets) - elif isinstance(bpm, np.ndarray): - if len(bpm) != len(unique_onsets) or bpm.ndim != 2: - raise ValueError( - f"`bpm` should be a 2D array with length {len(unique_onsets)}" - f" but has length {len(bpm}}" + if callable(bpm) or isinstance(bpm, np.ndarray): + + if callable(bpm): + # bpm parameter is a callable that returns a bpm value + # for each score onset + bp = 60 / bpm(unique_onsets) + bp_duration = ( + 60 / bpm(snote_array["onset_beat"]) * snote_array["duration_beat"] ) - # convert bpm to beat period - bp = 60 / float(bpm) - # TODO: allow for variable bpm and velocity - pnote_array["duration_sec"] = bp * snote_array["duration_beat"] + elif isinstance(bpm, np.ndarray): + + if bpm.ndim != 2: + raise ValueError("`bpm` should be a 2D array") + + bpm_fun = interp1d( + x=bpm[:, 0], + y=bpm[:, 1], + kind="linear", + bounds_error=False, + fill_value="interpolate", + ) + bp = 60 / bpm_fun(unique_onsets) + bp_duration = ( + 60 / bpm_fun(snote_array["onset_beat"]) * snote_array["duration_beat"] + ) + + p_onsets = np.r_[0, np.cumsum(iois * bp[:-1])] + pnote_array["duration_sec"] = bp_duration * snote_array["duration_beat"] + + else: + # convert bpm to beat period + bp = 60 / float(bpm) + p_onsets = np.r_[0, np.cumsum(iois * bp)] + pnote_array["duration_sec"] = bp * snote_array["duration_beat"] + pnote_array["pitch"] = snote_array["pitch"] pnote_array["id"] = snote_array["id"] - p_onsets = np.r_[0, np.cumsum(iois * bp)] for ix, on in zip(unique_onset_idxs, p_onsets): # ix has to be cast as integer depending on the diff --git a/tests/test_utils.py b/tests/test_utils.py index 44185eeb..4c569253 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,7 +3,8 @@ import numpy as np from partitura.utils import music -from tests import MATCH_IMPORT_EXPORT_TESTFILES, VOSA_TESTFILES +from tests import MATCH_IMPORT_EXPORT_TESTFILES, VOSA_TESTFILES, MOZART_VARIATION_FILES + RNG = np.random.RandomState(1984) @@ -127,3 +128,229 @@ def test_performance_from_part(self): # check that that the performance corresponds to the expected tempo self.assertTrue(np.allclose(60 / beat_period, bpm)) + + def get_tempo_curve(self, score_onsets, performance_onsets): + """ + Get tempo curve + """ + unique_sonsets = np.unique(score_onsets) + # Ensure that everything is sorted (I'm just paranoid ;) + unique_sonsets.sort() + unique_ponsets = np.unique(performance_onsets) + # Ensure that everything is sorted + unique_ponsets.sort() + + bp = np.diff(unique_ponsets) / np.diff(unique_sonsets) + + # Beats per minute for each of the unique onsets + # the last bpm is just assuming that the tempo remains + # constant after the last onset. + bpm = np.r_[60 / bp, 60 / bp[-1]] + + return bpm + + def test_performance_notearray_from_score_notearray_bpm(self): + """ + Test possibilities for bpm argument in + utils.music.performance_notearray_from_score_notearray + """ + score = partitura.load_score(MOZART_VARIATION_FILES["musicxml"]) + + score_note_array = score.note_array() + + unique_onsets = np.unique(score_note_array["onset_beat"]) + unique_onsets.sort() + # Test constant tempo + bpm = 30 + velocity = 65 + perf_note_array = music.performance_notearray_from_score_notearray( + snote_array=score_note_array, + bpm=bpm, + velocity=velocity, + ) + + self.assertTrue( + np.allclose( + self.get_tempo_curve( + score_note_array["onset_beat"], + perf_note_array["onset_sec"], + ), + bpm, + ) + ) + + # Test callable tempo + def bpm_fun(onset): + """ + Test function the first half of the piece will be played + twice as fast + """ + if isinstance(onset, (int, float)): + onset = np.array([onset]) + + bpm = np.zeros(len(onset), dtype=float) + + midpoint = (unique_onsets.max() - unique_onsets.min()) / 2 + bpm[np.where(onset <= midpoint)[0]] = 120 + bpm[np.where(onset > midpoint)[0]] = 60 + + return bpm + + perf_note_array = music.performance_notearray_from_score_notearray( + snote_array=score_note_array, + bpm=bpm_fun, + velocity=velocity, + ) + + bpm = self.get_tempo_curve( + score_note_array["onset_beat"], + perf_note_array["onset_sec"], + ) + + midpoint = (unique_onsets.max() - unique_onsets.min()) / 2 + + self.assertTrue( + np.allclose( + bpm[np.where(unique_onsets <= midpoint)[0]], + 120, + ) + ) + + self.assertTrue(np.allclose(bpm[np.where(unique_onsets > midpoint)[0]], 60)) + + # Test tempo as an array + bpm_expected = 40 * RNG.rand(len(unique_onsets)) + 30 + + # Test using 1d array + perf_note_array = music.performance_notearray_from_score_notearray( + snote_array=score_note_array, + bpm=np.column_stack((unique_onsets, bpm_expected)), + velocity=velocity, + ) + + bpm_predicted = self.get_tempo_curve( + score_note_array["onset_beat"], + perf_note_array["onset_sec"], + ) + + # do not consider the last element, since get_tempo_curve only computes + # the tempo up to the last onset (otherwise offsets need to be considered) + self.assertTrue(np.allclose(bpm_expected[:-1], bpm_predicted[:-1], atol=1e-3)) + + try: + # This should trigger an error because bpm_expected is a 1D array + perf_note_array = music.performance_notearray_from_score_notearray( + snote_array=score_note_array, + bpm=bpm_expected, + velocity=velocity, + ) + self.assertTrue(False) + + except ValueError: + # We are expecting the previous code to trigger an error + self.assertTrue(True) + + def get_velocity_curves(self, velocity, score_onsets): + """ + Get velocity curve by aggregating MIDI velocity values for + each onset + """ + unique_onsets = np.unique(score_onsets) + # Ensure that everything is sorted (I'm just paranoid ;) + unique_onsets.sort() + + unique_onset_idxs = [np.where(score_onsets == uo)[0] for uo in unique_onsets] + velocity_curve = np.array([velocity[ui].mean() for ui in unique_onset_idxs]) + + return velocity_curve + + def test_performance_notearray_from_score_notearray_velocity(self): + """ + Test velocity arguments in + utils.music.performance_notearray_from_score_notearray + """ + score = partitura.load_score(MOZART_VARIATION_FILES["musicxml"]) + + score_note_array = score.note_array() + + unique_onsets = np.unique(score_note_array["onset_beat"]) + unique_onsets.sort() + + # Test constant velocity + bpm = 120 + velocity = 65 + perf_note_array = music.performance_notearray_from_score_notearray( + snote_array=score_note_array, + bpm=bpm, + velocity=velocity, + ) + + self.assertTrue(all(perf_note_array["velocity"] == velocity)) + + # Test callable velocity + def vel_fun(onset): + """ + Test function the first half of the piece will be played + twice as loud + """ + if isinstance(onset, (int, float)): + onset = np.array([onset]) + + vel = np.zeros(len(onset), dtype=float) + + midpoint = (unique_onsets.max() - unique_onsets.min()) / 2 + vel[np.where(onset <= midpoint)[0]] = 120 + vel[np.where(onset > midpoint)[0]] = 60 + + return vel + + perf_note_array = music.performance_notearray_from_score_notearray( + snote_array=score_note_array, + bpm=bpm, + velocity=vel_fun, + ) + + vel = self.get_velocity_curves( + perf_note_array["velocity"], score_note_array["onset_beat"] + ) + + midpoint = (unique_onsets.max() - unique_onsets.min()) / 2 + + self.assertTrue( + np.allclose( + vel[np.where(unique_onsets <= midpoint)[0]], + 120, + ) + ) + + self.assertTrue(np.allclose(vel[np.where(unique_onsets > midpoint)[0]], 60)) + + # Test tempo as an array + vel_expected = np.round(40 * RNG.rand(len(unique_onsets)) + 30) + + # Test using 1d array + perf_note_array = music.performance_notearray_from_score_notearray( + snote_array=score_note_array, + velocity=np.column_stack((unique_onsets, vel_expected)), + bpm=bpm, + ) + + vel_predicted = self.get_velocity_curves( + perf_note_array["velocity"], + score_note_array["onset_beat"], + ) + + self.assertTrue(np.allclose(vel_expected, vel_predicted, atol=1e-3)) + + try: + # This should trigger an error because vel_expected is a 1D array + perf_note_array = music.performance_notearray_from_score_notearray( + snote_array=score_note_array, + bpm=bpm, + velocity=vel_expected, + ) + self.assertTrue(False) + + except ValueError: + # We are expecting the previous code to trigger an error + self.assertTrue(True) From 6520d84a21140bf4383dcade9bda707c95b2e140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Fri, 21 Oct 2022 15:28:35 +0200 Subject: [PATCH 29/41] update performance_from_part to use performance_notearray_from_scorenotearray --- partitura/utils/music.py | 87 +++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 51 deletions(-) diff --git a/partitura/utils/music.py b/partitura/utils/music.py index 1a78e7d5..e45e0ff8 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -2557,19 +2557,25 @@ def performance_from_part(part, bpm=100, velocity=64): ---------- part: Part The part from which we want to generate a performed part - bpm : float - Beats per minute - velocity: float or int - The MIDI velocity for all notes. + bpm : float, np.ndarray or callable + Beats per minute to generate the performance. If a the value is a float, + the performance will be generated with a constant tempo. If the value is + a np.ndarray, it has to be an array with two columns where the first + column is score time in beats and the second column is the tempo. If a + callable is given, the function is assumed to map score onsets in beats + to tempo values. Default is 100 bpm. + velocity: int, np.ndarray or callable + MIDI velocity of the performance. If a the value is an int, the + performance will be generated with a constant MIDI velocity. If the + value is a np.ndarray, it has to be an array with two columns where + the first column is score time in beats and the second column is the + MIDI velocity. If a callable is given, the function is assumed to map + score time in beats to MIDI velocity. Default is 64. Returns ------- ppart: PerformedPart - - Potential extensions - -------------------- - * allow for bpm to be a callable or an 2D array with columns (onset, bpm) - * allow for velocity to be a callable or a 2D array (onset, velocity) + A PerformedPart object with the generated performance. """ from partitura.score import Part from partitura.performance import PerformedPart @@ -2580,43 +2586,12 @@ def performance_from_part(part, bpm=100, velocity=64): f"`partitura.score.Part` instance, not {type(part)}" ) - ppart_fields = [ - ("onset_sec", "f4"), - ("duration_sec", "f4"), - ("pitch", "i4"), - ("velocity", "i4"), - ("track", "i4"), - ("channel", "i4"), - ("id", "U256"), - ] snote_array = part.note_array() - pnote_array = np.zeros(len(snote_array), dtype=ppart_fields) - - unique_onsets = np.unique(snote_array["onset_beat"]) - # Cast as object to avoid warnings, but seems to work well - # in numpy version 1.20.1 - unique_onset_idxs = np.array( - [np.where(snote_array["onset_beat"] == u)[0] for u in unique_onsets], - dtype=object, + pnote_array = performance_notearray_from_score_notearray( + snote_array=snote_array, bpm=bpm, velocity=velocity ) - iois = np.diff(unique_onsets) - - bp = 60 / float(bpm) - - # TODO: allow for variable bpm and velocity - pnote_array["duration_sec"] = bp * snote_array["duration_beat"] - pnote_array["velocity"] = int(velocity) - pnote_array["pitch"] = snote_array["pitch"] - pnote_array["id"] = snote_array["id"] - p_onsets = np.r_[0, np.cumsum(iois * bp)] - - for ix, on in zip(unique_onset_idxs, p_onsets): - # ix has to be cast as integer depending on the - # numpy version... - pnote_array["onset_sec"][ix.astype(int)] = on - ppart = PerformedPart.from_note_array(pnote_array) return ppart @@ -2634,11 +2609,21 @@ def performance_notearray_from_score_notearray( ---------- snote_array : np.ndarray A score note array. - bpm : float - Beats per minute to generate the performance. Default is 100. - velocity: int or np.ndarray - The MIDI velocity for all notes. If a numpy array is given, it should - be the same length as `snote_array`. + bpm : float, np.ndarray or callable + Beats per minute to generate the performance. If a the value is a float, + the performance will be generated with a constant tempo. If the value is + a np.ndarray, it has to be an array with two columns where the first + column is score time in beats and the second column is the tempo. If a + callable is given, the function is assumed to map score onsets in beats + to tempo values. Default is 100 bpm. + velocity: int, np.ndarray or callable + MIDI velocity of the performance. If a the value is an int, the + performance will be generated with a constant MIDI velocity. If the + value is a np.ndarray, it has to be an array with two columns where + the first column is score time in beats and the second column is the + MIDI velocity. If a callable is given, the function is assumed to map + score time in beats to MIDI velocity. Default is 64. + Returns ------- @@ -2666,9 +2651,9 @@ def performance_notearray_from_score_notearray( velocity_fun = interp1d( x=velocity[:, 0], y=velocity[:, 1], - kind="linear", + kind="previous", bounds_error=False, - fill_value="interpolate", + fill_value=(velocity[0, 1], velocity[-1, 1]), ) pnote_array["velocity"] = np.round( velocity_fun(snote_array["onset_beat"]), @@ -2715,9 +2700,9 @@ def performance_notearray_from_score_notearray( bpm_fun = interp1d( x=bpm[:, 0], y=bpm[:, 1], - kind="linear", + kind="previous", bounds_error=False, - fill_value="interpolate", + fill_value=(bpm[0, 1], bpm[-1, 1]), ) bp = 60 / bpm_fun(unique_onsets) bp_duration = ( From 96e5be5d459722fd18eea9f3d2a2ce562c2e189e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Fri, 21 Oct 2022 16:39:30 +0200 Subject: [PATCH 30/41] fix issue #166 --- partitura/utils/synth.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/partitura/utils/synth.py b/partitura/utils/synth.py index 9b06c31c..14361182 100644 --- a/partitura/utils/synth.py +++ b/partitura/utils/synth.py @@ -17,6 +17,7 @@ ensure_notearray, get_time_units_from_note_array, midi_pitch_to_frequency, + performance_notearray_from_score_notearray, ) TWO_PI = 2 * np.pi @@ -72,7 +73,7 @@ def midi_pitch_to_natural_frequency( perfect fourth computed with respect to A5, etc.). """ - + octave = (midi_pitch // 12) - 1 aref = 69.0 - 12.0 * (4 - octave) @@ -105,7 +106,7 @@ def midi_pitch_to_tempered_frequency( interval_ratios: Dict[int, float] = NATURAL_INTERVAL_RATIOS, ) -> Union[float, np.ndarray]: """ - Convert MIDI pitch to frequency in Hz using + Convert MIDI pitch to frequency in Hz using a temperament given as frequency ratios above a reference pitch. @@ -133,12 +134,7 @@ def midi_pitch_to_tempered_frequency( if isinstance(interval, (int, float)): interval = np.array([interval], dtype=int) - ratios = np.array( - [ - interval_ratios[abs(itv)] - for itv in interval - ] - ) + ratios = np.array([interval_ratios[abs(itv)] for itv in interval]) freqs = adjusted_reference_frequency * ratios @@ -321,7 +317,8 @@ def synthesize( tuning: Union[str, Callable] = "equal_temperament", tuning_kwargs: Dict[str, Any] = {"a4": A4}, harmonic_dist: Optional[Union[str, int]] = None, - bpm: Union[float, int] = 60, + bpm: Union[float, np.ndarray, Callable] = 60, + velocity: Union[int, np.ndarray, Callable] = 64, ) -> np.ndarray: """ Synthesize a partitura object with note information @@ -337,12 +334,19 @@ def synthesize( envelope_fun: {"linear", "exp" } The type of envelop to apply to the individual sine waves. tuning: {"equal_temperament", "natural"} + The tuning system to use. harmonic_dist : int, "shepard" or None (optional) Distribution of harmonics. If an integer, it is the number of harmonics to be considered. If "shepard", it uses Shepard tones. Default is None (i.e., only consider the fundamental frequency) - bpm : int - The bpm to render the output (if the input is a score-like object) + bpm : float, np.ndarray or callable + The bpm to render the output (if the input is a score-like object). + See `partitura.utils.music.performance_notearray_from_score_notearray` + for more information on this parameter. + velocity: int, np.ndarray or callable + The MIDI velocity to render the output (if the input is a score-like object). + See `partitura.utils.music.performance_notearray_from_score_notearray` + for more information on this parameter. Returns ------- @@ -358,10 +362,14 @@ def synthesize( # If the input is a score, convert score time to seconds if onset_unit != "onset_sec": - beat2sec = 60 / bpm - onsets = note_array[onset_unit] * beat2sec - offsets = (note_array[onset_unit] + note_array[duration_unit]) * beat2sec - duration = note_array[duration_unit] * beat2sec + pnote_array = performance_notearray_from_score_notearray( + snote_array=note_array, + bpm=bpm, + velocity=velocity, + ) + onsets = pnote_array["onset_sec"] + offsets = pnote_array["onset_sec"] + pnote_array["duration_sec"] + duration = pnote_array["duration_sec"] else: onsets = note_array["onset_sec"] offsets = note_array["onset_sec"] + note_array["duration_sec"] From 001fda4263bf88e945d191c1cc77a22f66c17781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Fri, 21 Oct 2022 16:59:43 +0200 Subject: [PATCH 31/41] update documentation for save_wav --- partitura/io/exportaudio.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/partitura/io/exportaudio.py b/partitura/io/exportaudio.py index 686cf5f0..eeeb5dbc 100644 --- a/partitura/io/exportaudio.py +++ b/partitura/io/exportaudio.py @@ -5,6 +5,7 @@ import numpy as np from scipy.io import wavfile + from partitura.score import ScoreLike from partitura.performance import PerformanceLike @@ -23,7 +24,7 @@ def save_wav( tuning: Union[str, Callable] = "equal_temperament", tuning_kwargs: Dict[str, Any] = {"a4": A4}, harmonic_dist: Optional[Union[str, int]] = None, - bpm: Union[float, int] = 60, + bpm: Union[float, np.ndarray, Callable] = 60, ) -> Optional[np.ndarray]: """ Export a score (a `Score`, `Part`, `PartGroup` or list of `Part` instances), @@ -59,8 +60,10 @@ def save_wav( Distribution of harmonics. If an integer, it is the number of harmonics to be considered. If "shepard", it uses Shepard tones. Default is None (i.e., only consider the fundamental frequency) - bpm : int - The bpm to render the output (if the input is a score-like object) + bpm : float, np.ndarray, callable + The bpm to render the output (if the input is a score-like object). + See `partitura.utils.music.performance_notearray_from_score_notearray` + for more information on this parameter. Returns ------- From 4d50700845f3ead391bd6efc10b95686109f1385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Fri, 21 Oct 2022 17:01:24 +0200 Subject: [PATCH 32/41] remove unused velocity argument --- partitura/utils/synth.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/partitura/utils/synth.py b/partitura/utils/synth.py index 14361182..e3e88c02 100644 --- a/partitura/utils/synth.py +++ b/partitura/utils/synth.py @@ -318,7 +318,6 @@ def synthesize( tuning_kwargs: Dict[str, Any] = {"a4": A4}, harmonic_dist: Optional[Union[str, int]] = None, bpm: Union[float, np.ndarray, Callable] = 60, - velocity: Union[int, np.ndarray, Callable] = 64, ) -> np.ndarray: """ Synthesize a partitura object with note information @@ -343,10 +342,6 @@ def synthesize( The bpm to render the output (if the input is a score-like object). See `partitura.utils.music.performance_notearray_from_score_notearray` for more information on this parameter. - velocity: int, np.ndarray or callable - The MIDI velocity to render the output (if the input is a score-like object). - See `partitura.utils.music.performance_notearray_from_score_notearray` - for more information on this parameter. Returns ------- From aa7c48905f3bfbc52020487986b224d1037ddec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Sat, 22 Oct 2022 04:49:55 +0200 Subject: [PATCH 33/41] fix remove unused velocity parameter --- partitura/utils/synth.py | 1 - 1 file changed, 1 deletion(-) diff --git a/partitura/utils/synth.py b/partitura/utils/synth.py index e3e88c02..9de84566 100644 --- a/partitura/utils/synth.py +++ b/partitura/utils/synth.py @@ -360,7 +360,6 @@ def synthesize( pnote_array = performance_notearray_from_score_notearray( snote_array=note_array, bpm=bpm, - velocity=velocity, ) onsets = pnote_array["onset_sec"] offsets = pnote_array["onset_sec"] + pnote_array["duration_sec"] From b8f1fb8af97e3237c73e860c2bee679e1e0c79c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Sat, 22 Oct 2022 07:52:32 +0200 Subject: [PATCH 34/41] Declare encoding and add/update docstrings at the top of each module --- partitura/__init__.py | 6 +- partitura/directions.py | 14 +- partitura/display.py | 6 +- partitura/io/__init__.py | 5 + partitura/io/exportaudio.py | 5 +- partitura/io/exportmatch.py | 2 +- partitura/io/exportmidi.py | 7 +- partitura/io/exportmusicxml.py | 5 +- partitura/io/importkern.py | 5 + partitura/io/importmei.py | 5 + partitura/io/importmidi.py | 4 + partitura/io/importmusicxml.py | 4 +- partitura/io/importnakamura.py | 1 + partitura/io/musescore.py | 7 +- partitura/musicanalysis/__init__.py | 5 +- partitura/musicanalysis/key_identification.py | 7 +- partitura/musicanalysis/meter.py | 143 ++++++++++-------- partitura/musicanalysis/note_features.py | 5 + partitura/musicanalysis/performance_codec.py | 6 + partitura/musicanalysis/pitch_spelling.py | 7 +- partitura/musicanalysis/tonal_tension.py | 4 +- partitura/musicanalysis/voice_separation.py | 11 +- partitura/performance.py | 5 +- partitura/score.py | 7 +- partitura/utils/__init__.py | 7 +- partitura/utils/generic.py | 7 +- partitura/utils/misc.py | 5 + partitura/utils/music.py | 4 + partitura/utils/synth.py | 7 +- tests/__init__.py | 3 +- tests/test_deprecations.py | 5 + tests/test_kern.py | 8 +- tests/test_key_estimation.py | 5 + tests/test_load_performance.py | 8 +- tests/test_load_score.py | 6 +- tests/test_match_import.py | 91 +++++++++-- tests/test_mei.py | 4 +- tests/test_merge_parts.py | 4 + tests/test_metrical_position.py | 6 +- tests/test_midi_export.py | 5 +- tests/test_midi_import.py | 5 +- tests/test_nakamura.py | 6 +- tests/test_new_divs.py | 5 +- tests/test_note_array.py | 3 +- tests/test_note_features.py | 5 + tests/test_parangonada.py | 6 +- tests/test_part_properties.py | 5 + tests/test_performance.py | 6 +- tests/test_performance_codec.py | 6 +- tests/test_pianoroll.py | 4 + tests/test_pitch_spelling.py | 5 + tests/test_quarter_adjust.py | 4 + tests/test_rest_array.py | 3 +- tests/test_synth.py | 21 ++- tests/test_time_estimation.py | 7 +- tests/test_times.py | 5 + tests/test_tonal_tension.py | 5 + tests/test_utils.py | 5 + tests/test_voice_estimation.py | 5 + tests/test_xml.py | 6 +- 60 files changed, 405 insertions(+), 163 deletions(-) diff --git a/partitura/__init__.py b/partitura/__init__.py index 62ea9e1c..f278849c 100644 --- a/partitura/__init__.py +++ b/partitura/__init__.py @@ -1,7 +1,9 @@ -"""The top level of the package contains functions to load and save +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +The top level of the package contains functions to load and save data, display rendered scores, and functions to estimate pitch spelling, voice assignment, and key signature. - """ import pkg_resources diff --git a/partitura/directions.py b/partitura/directions.py index e0bce335..6c2cd289 100644 --- a/partitura/directions.py +++ b/partitura/directions.py @@ -1,12 +1,12 @@ #!/usr/bin/env python - +# -*- coding: utf-8 -*- """ -Parse textual directions that occur in a score (in a MusicXML they are -encoded as ), and if possible, convert them to a specific -score.Direction class or subclass. For example "cresc." will produce a -`score.DynamicLoudnessDirection` instance, and "Allegro molto" will produce a -`score.ConstantTempoDirection` instance. If the meaning of the direction cannot -be inferred, a `score.Words` instance is returned. +This module contains methods to Parse textual directions that occur in a score +(in a MusicXML they are encoded as ), and if possible, convert +them to a specific score.Direction class or subclass. For example "cresc." will +produce a `score.DynamicLoudnessDirection` instance, and "Allegro molto" will +produce a `score.ConstantTempoDirection` instance. If the meaning of the +direction cannot be inferred, a `score.Words` instance is returned. The functionality is provided by the function `parse_words` """ diff --git a/partitura/display.py b/partitura/display.py index 61a52378..31ece28a 100644 --- a/partitura/display.py +++ b/partitura/display.py @@ -1,9 +1,9 @@ #!/usr/bin/env python - -"""This module defines a function "show" that creates a rendering of one +# -*- coding: utf-8 -*- +""" +This module defines a function "show" that creates a rendering of one or more parts or partgroups and opens it using the desktop default application. - """ import platform diff --git a/partitura/io/__init__.py b/partitura/io/__init__.py index ba21845f..e90c82d5 100644 --- a/partitura/io/__init__.py +++ b/partitura/io/__init__.py @@ -1,3 +1,8 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +This module contains methods for importing and exporting symbolic music formats. +""" from typing import Union from .importmusicxml import load_musicxml diff --git a/partitura/io/exportaudio.py b/partitura/io/exportaudio.py index 61ede0ca..ee5f97e4 100644 --- a/partitura/io/exportaudio.py +++ b/partitura/io/exportaudio.py @@ -1,5 +1,8 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- """ -Synthesize Partitura object to wav using additive synthesis +This module contains methods to synthesize Partitura object to wav using +additive synthesis """ from typing import Union, Optional import numpy as np diff --git a/partitura/io/exportmatch.py b/partitura/io/exportmatch.py index 428f3b6c..f8b847cd 100644 --- a/partitura/io/exportmatch.py +++ b/partitura/io/exportmatch.py @@ -5,7 +5,7 @@ """ import numpy as np -from typing import List, Optional, Union, Iterable +from typing import List, Optional, Iterable from scipy.interpolate import interp1d from partitura.io.importmatch import ( diff --git a/partitura/io/exportmidi.py b/partitura/io/exportmidi.py index 91e64da3..1e8740ec 100644 --- a/partitura/io/exportmidi.py +++ b/partitura/io/exportmidi.py @@ -1,9 +1,12 @@ #!/usr/bin/env python - +# -*- coding: utf-8 -*- +""" +This module contains methods for exporting MIDI files +""" import numpy as np from collections import defaultdict, OrderedDict -from typing import Union, Optional, Iterable +from typing import Optional, Iterable from mido import MidiFile, MidiTrack, Message, MetaMessage diff --git a/partitura/io/exportmusicxml.py b/partitura/io/exportmusicxml.py index be06abf5..2434c291 100644 --- a/partitura/io/exportmusicxml.py +++ b/partitura/io/exportmusicxml.py @@ -1,5 +1,8 @@ #!/usr/bin/env python - +# -*- coding: utf-8 -*- +""" +This module contains methods for exporting MusicXML files. +""" import math from collections import defaultdict from lxml import etree diff --git a/partitura/io/importkern.py b/partitura/io/importkern.py index f10b546e..361b32a4 100644 --- a/partitura/io/importkern.py +++ b/partitura/io/importkern.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains methods for importing Humdrum Kern files. +""" import re import warnings diff --git a/partitura/io/importmei.py b/partitura/io/importmei.py index 3bffa464..0995b1b1 100644 --- a/partitura/io/importmei.py +++ b/partitura/io/importmei.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains methods for importing MEI files. +""" from lxml import etree from xmlschema.names import XML_NAMESPACE import partitura.score as score diff --git a/partitura/io/importmidi.py b/partitura/io/importmidi.py index 190a03ec..ffa6e97f 100644 --- a/partitura/io/importmidi.py +++ b/partitura/io/importmidi.py @@ -1,4 +1,8 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains methods for importing MIDI files. +""" import warnings from collections import defaultdict diff --git a/partitura/io/importmusicxml.py b/partitura/io/importmusicxml.py index b5c44b9e..f4235e0c 100644 --- a/partitura/io/importmusicxml.py +++ b/partitura/io/importmusicxml.py @@ -1,6 +1,8 @@ #!/usr/bin/env python - # -*- coding: utf-8 -*- +""" +This module contains methods for importing MusicXML files. +""" import os import warnings diff --git a/partitura/io/importnakamura.py b/partitura/io/importnakamura.py index cf3b9fe2..22e82c34 100644 --- a/partitura/io/importnakamura.py +++ b/partitura/io/importnakamura.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # -*- coding: utf-8 -*- """ This module contains methods for parsing score-to-performance alignments diff --git a/partitura/io/musescore.py b/partitura/io/musescore.py index 36d115a8..f5b2eb76 100644 --- a/partitura/io/musescore.py +++ b/partitura/io/musescore.py @@ -1,8 +1,8 @@ #!/usr/bin/env python - -"""This module contains functionality to use the MuseScore program as a +# -*- coding: utf-8 -*- +""" +This module contains functionality to use the MuseScore program as a backend for loading and rendering scores. - """ import platform @@ -25,7 +25,6 @@ ) - class MuseScoreNotFoundException(Exception): pass diff --git a/partitura/musicanalysis/__init__.py b/partitura/musicanalysis/__init__.py index 4385a324..7c684ed8 100644 --- a/partitura/musicanalysis/__init__.py +++ b/partitura/musicanalysis/__init__.py @@ -1,8 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Tools for music analysis. - +This module contains tools for estimating key signature, time signature, +pitch spelling, voice information, tonal tension, as well as methods for +deriving note-level features and performance encodings. """ from .voice_separation import estimate_voices diff --git a/partitura/musicanalysis/key_identification.py b/partitura/musicanalysis/key_identification.py index ffb3dde7..1fc5fa35 100644 --- a/partitura/musicanalysis/key_identification.py +++ b/partitura/musicanalysis/key_identification.py @@ -1,7 +1,12 @@ +#!/usr/bin/env python # -*- coding: utf-8 -*- """ -Krumhansl and Shepard key estimation +This module implements Krumhansl and Schmuckler key estimation method. +References +---------- +.. [2] Krumhansl, Carol L. (1990) "Cognitive foundations of musical pitch", + Oxford University Press, New York. """ import numpy as np from scipy.linalg import circulant diff --git a/partitura/musicanalysis/meter.py b/partitura/musicanalysis/meter.py index ba8d6f86..eff61c8c 100644 --- a/partitura/musicanalysis/meter.py +++ b/partitura/musicanalysis/meter.py @@ -1,21 +1,22 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Meter numerator, Beat, and Tempo estimation. +This module implements methods for Meter numerator, Beat, and Tempo estimation. -Implementation adapted from Jakob Woegerbauer +Implementation adapted from Jakob Woegerbauer based on a model published by Simon Dixon.[1] References ---------- -.. [1] Simon Dixon (2001), Automatic extraction of +.. [1] Simon Dixon (2001), Automatic extraction of tempo and beat from expressive performances. Journal of New Music Research, 30(1):39–58 """ import warnings import numpy as np + # import scipy.spatial.distance as distance # from scipy.interpolate import interp1d @@ -26,24 +27,25 @@ MAX = 9999999999999 MIN_INTERVAL = 0.01 MAX_INTERVAL = 2 # in seconds -CLUSTER_WIDTH = 1/12 # in seconds +CLUSTER_WIDTH = 1 / 12 # in seconds N_CLUSTERS = 100 INIT_DURATION = 10 # in seconds TIMEOUT = 10 # in seconds TOLERANCE_POST = 0.4 # propotion of beat_interval TOLERANCE_PRE = 0.2 # proportion of beat_interval -TOLERANCE_INNER = 1/12 -CORRECTION_FACTOR = 1/4 # higher => more correction (speed changes) +TOLERANCE_INNER = 1 / 12 +CORRECTION_FACTOR = 1 / 4 # higher => more correction (speed changes) MAX_AGENTS = 100 # delete low-scoring agents when there are more than MAX_AGENTS -CHORD_SPREAD_TIME = 1/12 # for onset aggregation +CHORD_SPREAD_TIME = 1 / 12 # for onset aggregation -class MultipleAgents(): +class MultipleAgents: """ Class to compute inter onset interval clusters - and to instantiate a number of agents to + and to instantiate a number of agents to approximate beat positions. """ + def run(self, onsets, salience): self.clusters = [] self.agents = [] @@ -72,9 +74,9 @@ def setup_clusters(self, onsets): # create inter-onset interval clusters self.clusters = [] for i in range(len(onsets)): - for j in range(i+1, len(onsets)): - ioi = onsets[j]-onsets[i] - if ioi < MIN_INTERVAL: + for j in range(i + 1, len(onsets)): + ioi = onsets[j] - onsets[i] + if ioi < MIN_INTERVAL: continue if ioi > MAX_INTERVAL: break @@ -92,7 +94,7 @@ def setup_clusters(self, onsets): i = 0 while i < len(self.clusters): c_i = self.clusters[i] - i = i+1 + i = i + 1 j = i while j < len(self.clusters): if abs(c_i.interval - self.clusters[j].interval) < CLUSTER_WIDTH: @@ -109,12 +111,12 @@ def setup_clusters(self, onsets): c.interval *= 2 while c.interval > MAX_INTERVAL: c.interval /= 2 - + # merge again i = 0 while i < len(self.clusters): c_i = self.clusters[i] - i = i+1 + i = i + 1 j = i while j < len(self.clusters): if abs(c_i.interval - self.clusters[j].interval) < CLUSTER_WIDTH: @@ -127,11 +129,12 @@ def setup_clusters(self, onsets): for c_i in self.clusters: for c_j in self.clusters: n = round(c_j.interval / c_i.interval) - if abs(c_i.interval - n*c_j.interval) < CLUSTER_WIDTH: + if abs(c_i.interval - n * c_j.interval) < CLUSTER_WIDTH: c_i.score += Cluster.relationship_factor(n) * len(c_j.iois) self.clusters = sorted(self.clusters, key=lambda x: x.score, reverse=True)[ - :N_CLUSTERS] + :N_CLUSTERS + ] def init_tracking(self, onsets, salience): self.agents = [] @@ -146,7 +149,7 @@ def init_tracking(self, onsets, salience): self.agents.append(a) i += 1 - def track(self, onsets, salience): + def track(self, onsets, salience): for e_i in range(len(onsets)): e = onsets[e_i] new_agents = [] @@ -155,10 +158,13 @@ def track(self, onsets, salience): if e - a.lastBeat() > TIMEOUT: remove_agents.append(a) else: - while a.prediction + TOLERANCE_POST*a.beat_interval < e: - a.history.append((a.prediction, 0)) + while a.prediction + TOLERANCE_POST * a.beat_interval < e: + a.history.append((a.prediction, 0)) a.prediction += a.beat_interval - if a.prediction - TOLERANCE_PRE*a.beat_interval <= e and e <= a.prediction + TOLERANCE_POST*a.beat_interval: + if ( + a.prediction - TOLERANCE_PRE * a.beat_interval <= e + and e <= a.prediction + TOLERANCE_POST * a.beat_interval + ): if abs(a.prediction - e) > TOLERANCE_INNER: a_new = Agent() a_new.beat_interval = a.beat_interval @@ -167,10 +173,12 @@ def track(self, onsets, salience): a_new.score = a.score new_agents.append(a_new) err = e - a.prediction - a.beat_interval = a.beat_interval + err*CORRECTION_FACTOR + a.beat_interval = a.beat_interval + err * CORRECTION_FACTOR a.prediction = e + a.beat_interval a.history.append((e, salience[e_i])) - a.score += (1-abs(err/a.beat_interval)/2.) * salience[e_i] + a.score += (1 - abs(err / a.beat_interval) / 2.0) * salience[ + e_i + ] for a in remove_agents: self.agents.remove(a) @@ -181,35 +189,42 @@ def track(self, onsets, salience): agents_all = self.agents[:] self.agents = [] for i in range(len(agents_all)): - for j in range(i+1, len(agents_all)): + for j in range(i + 1, len(agents_all)): if duplicate[i] > 0 or duplicate[j] > 0: continue - if abs(agents_all[i].beat_interval - agents_all[j].beat_interval) < 0.01 \ - and abs(agents_all[i].lastBeat() - agents_all[j].lastBeat()) < 0.02: + if ( + abs(agents_all[i].beat_interval - agents_all[j].beat_interval) + < 0.01 + and abs(agents_all[i].lastBeat() - agents_all[j].lastBeat()) + < 0.02 + ): if agents_all[i].score > agents_all[j].score: duplicate[j] += 1 else: duplicate[i] += 1 break - self.agents = sorted(np.asarray(agents_all)[(duplicate < 1)].tolist( - ), key=lambda x: x.score, reverse=True)[:MAX_AGENTS] + self.agents = sorted( + np.asarray(agents_all)[(duplicate < 1)].tolist(), + key=lambda x: x.score, + reverse=True, + )[:MAX_AGENTS] self.agents = sorted(self.agents, key=lambda x: x.score, reverse=True) -class Cluster(): +class Cluster: """ Class for inter onset interval clusters. - + Parameters ---------- ioi : float - an initial inter onset interval - + an initial inter onset interval + """ - + def __init__(self, ioi) -> None: self.iois = np.zeros(0) self.score = 0 @@ -217,24 +232,25 @@ def __init__(self, ioi) -> None: self.addIoi(ioi) def getK(self, ioi): - diff = abs(self.interval-ioi) + diff = abs(self.interval - ioi) if diff < CLUSTER_WIDTH: return diff return False def addIoi(self, ioi): self.iois = np.append(self.iois, ioi) - self.interval = np.sum(self.iois)/len(self.iois) + self.interval = np.sum(self.iois) / len(self.iois) @staticmethod def relationship_factor(d): if 1 <= d and d <= 4: - return 6-d + return 6 - d elif 5 <= d and d <= 8: return 1 return 0 -class Agent(): + +class Agent: """ Class for beat induction agents. """ @@ -246,43 +262,47 @@ def __init__(self) -> None: self.score = 0 def lastBeat(self): - i = len(self.history)-1 + i = len(self.history) - 1 while i > 0 and self.history[i][1] == 0: - i-=1 + i -= 1 return self.history[i][0] def getTempo(self): - return 60.0 * (len(self.history)-1) / (self.history[-1][0]-self.history[0][0]) - + return ( + 60.0 * (len(self.history) - 1) / (self.history[-1][0] - self.history[0][0]) + ) + def getTimeSignatureNum(self): possibleNums = [2, 3, 4, 6, 9, 12, 24] - bestVal = {num:0 for num in possibleNums} + bestVal = {num: 0 for num in possibleNums} salience = list(zip(*self.history))[1] sumSalience = sum(salience) f = 1.005 for num in possibleNums: - for startIdx in range(num): + for startIdx in range(num): dbs = len(salience[startIdx::num]) if dbs > 1: - downbeatSalience = sum(salience[startIdx::num])/dbs - sumSalience = sum(salience[:(dbs-1)*num]) - otherSalience = (sumSalience-downbeatSalience*dbs)/((num-1)*(dbs-1)) + downbeatSalience = sum(salience[startIdx::num]) / dbs + sumSalience = sum(salience[: (dbs - 1) * num]) + otherSalience = (sumSalience - downbeatSalience * dbs) / ( + (num - 1) * (dbs - 1) + ) else: downbeatSalience = 0 otherSalience = 1 - - ratio = downbeatSalience/otherSalience + + ratio = downbeatSalience / otherSalience bestVal[num] = max(bestVal[num], ratio) - bestNum = max(bestVal, key=bestVal.get) - + bestNum = max(bestVal, key=bestVal.get) + return bestNum def estimate_time(note_info): """ Estimate tempo, meter (currently only time signature numerator), and beats - + Parameters ---------- note_info : structured array, `Part` or `PerformedPart` @@ -293,19 +313,19 @@ def estimate_time(note_info): onset and duration information of both score and performance, (e.g., containing both `onset_beat` and `onset_sec`), the score information will be preferred. - + Returns ------- dict Tempo, meter, and beat information - """ + """ note_array = ensure_notearray(note_info) onset_kw, _ = get_time_units_from_note_array(note_array) onsets_raw = note_array[onset_kw] - + # aggregate notes in clusters - aggregated_notes = [(0,0)] + aggregated_notes = [(0, 0)] for note_on in onsets_raw: prev_note_on = aggregated_notes[-1][0] prev_note_salience = aggregated_notes[-1][1] @@ -313,14 +333,11 @@ def estimate_time(note_info): aggregated_notes[-1] = (note_on, prev_note_salience + 1) else: aggregated_notes.append((note_on, 1)) - - print(aggregated_notes) + + print(aggregated_notes) onsets, saliences = list(zip(*aggregated_notes)) - + ma = MultipleAgents() ma.run(onsets, saliences) - - return dict(tempo=ma.getTempo(), - meter_numerator=ma.getNum(), - beats=ma.getBeats()) - \ No newline at end of file + + return dict(tempo=ma.getTempo(), meter_numerator=ma.getNum(), beats=ma.getBeats()) diff --git a/partitura/musicanalysis/note_features.py b/partitura/musicanalysis/note_features.py index bce4be86..58d0f9d0 100644 --- a/partitura/musicanalysis/note_features.py +++ b/partitura/musicanalysis/note_features.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains methods to compute note-level features. +""" import sys import warnings import numpy as np diff --git a/partitura/musicanalysis/performance_codec.py b/partitura/musicanalysis/performance_codec.py index 64957e1d..59cebae1 100644 --- a/partitura/musicanalysis/performance_codec.py +++ b/partitura/musicanalysis/performance_codec.py @@ -1,3 +1,9 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module implements a codec to encode and decode expressive performances to a set of +expressive parameters. +""" import numpy as np import numpy.lib.recfunctions as rfn try: diff --git a/partitura/musicanalysis/pitch_spelling.py b/partitura/musicanalysis/pitch_spelling.py index 191492e3..d2470dc7 100644 --- a/partitura/musicanalysis/pitch_spelling.py +++ b/partitura/musicanalysis/pitch_spelling.py @@ -1,11 +1,12 @@ +#!/usr/bin/env python # -*- coding: utf-8 -*- """ -Pitch Spelling using the ps13 algorithm. +This module contains methods for estimation pitch spelling using the ps13 algorithm. References ---------- - - +.. [4] Meredith, D. (2006). "The ps13 Pitch Spelling Algorithm". Journal + of New Music Research, 35(2):121. """ import numpy as np from collections import namedtuple diff --git a/partitura/musicanalysis/tonal_tension.py b/partitura/musicanalysis/tonal_tension.py index 7c3b7232..08778647 100644 --- a/partitura/musicanalysis/tonal_tension.py +++ b/partitura/musicanalysis/tonal_tension.py @@ -1,8 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Spiral array representation and tonal tension profiles using Herreman and -Chew's tension ribbons +This module contains methods to compute Chew's spiral array representation +and the tonal tension profiles using Herreman and Chew's tension ribbons References ---------- diff --git a/partitura/musicanalysis/voice_separation.py b/partitura/musicanalysis/voice_separation.py index e0328d50..d714e807 100644 --- a/partitura/musicanalysis/voice_separation.py +++ b/partitura/musicanalysis/voice_separation.py @@ -1,7 +1,14 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -"""Voice Separation using Chew and Wu's algorithm. - +""" +This module contains methods for voice separation using Chew and Wu's algorithm. + +References +---------- +.. [6] Chew, E. and Wu, Xiaodan (2004) "Separating Voices in + Polyphonic Music: A Contig Mapping Approach". In Uffe Kock, + editor, "Computer Music Modeling and Retrieval". Springer + Berlin Heidelberg. """ from collections import defaultdict from statistics import mode diff --git a/partitura/performance.py b/partitura/performance.py index 1818eac0..c0482944 100644 --- a/partitura/performance.py +++ b/partitura/performance.py @@ -1,11 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - -"""This module contains a lightweight ontology to represent a performance in a +""" +This module contains a lightweight ontology to represent a performance in a MIDI-like format. A performance is defined at the highest level by a :class:`~partitura.performance.PerformedPart`. This object contains performed notes as well as continuous control parameters, such as sustain pedal. - """ diff --git a/partitura/score.py b/partitura/score.py index 18548291..95f533ad 100644 --- a/partitura/score.py +++ b/partitura/score.py @@ -1,13 +1,12 @@ +#!/usr/bin/python # -*- coding: utf-8 -*- - - -"""This module defines an ontology of musical elements to represent +""" +This module defines an ontology of musical elements to represent musical scores, such as measures, notes, slurs, words, tempo and loudness directions. A score is defined at the highest level by a `Part` object (or a hierarchy of `Part` objects, in a `PartGroup` object). This object serves as a timeline at which musical elements are registered in terms of their start and end times. - """ from copy import copy diff --git a/partitura/utils/__init__.py b/partitura/utils/__init__.py index 2e8edd76..e43b6280 100644 --- a/partitura/utils/__init__.py +++ b/partitura/utils/__init__.py @@ -1,4 +1,8 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Top level of the utilities module. +""" from partitura.utils.generic import ( ComparableMixin, @@ -51,7 +55,7 @@ synthesize ) -from .misc import ( +from partitura.utils.misc import ( PathLike, get_document_name, deprecated_alias, @@ -72,4 +76,5 @@ "pitch_spelling_to_note_name", "show_diff", "PrettyPrintTree", + "synthesize", ] diff --git a/partitura/utils/generic.py b/partitura/utils/generic.py index 36b8b5cf..6ac2e919 100644 --- a/partitura/utils/generic.py +++ b/partitura/utils/generic.py @@ -1,5 +1,8 @@ -#!/usr/bin/env python - +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +This module contains generic class- and numerical-related utilities +""" import warnings from collections import defaultdict from textwrap import dedent diff --git a/partitura/utils/misc.py b/partitura/utils/misc.py index 9819f77e..da02564d 100644 --- a/partitura/utils/misc.py +++ b/partitura/utils/misc.py @@ -1,3 +1,8 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +This module contains miscellaneous utilities. +""" import functools import os import warnings diff --git a/partitura/utils/music.py b/partitura/utils/music.py index c16adb64..8c2e49ce 100644 --- a/partitura/utils/music.py +++ b/partitura/utils/music.py @@ -1,4 +1,8 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains music related utilities +""" from collections import defaultdict import re import warnings diff --git a/partitura/utils/synth.py b/partitura/utils/synth.py index 0d53765a..bfca744c 100644 --- a/partitura/utils/synth.py +++ b/partitura/utils/synth.py @@ -1,9 +1,12 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- """ -Synthesize Partitura Part or Note array to wav using additive synthesis +This module contains methods for synthesizing score- or performance-like +objects using additive synthesis TODO +---- * Add other tuning systems? - """ from typing import Union, Tuple, Dict, Optional, Any, Callable diff --git a/tests/__init__.py b/tests/__init__.py index f3c3b6ad..ea98732e 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,5 @@ -# encoding: utf-8 +#!/usr/bin/env python +# -*- coding: utf-8 -*- # pylint: skip-file """ This module contains tests. diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py index 3130c9f1..606c0759 100644 --- a/tests/test_deprecations.py +++ b/tests/test_deprecations.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module includes tests for deprecation utilities. +""" import unittest import warnings import numpy as np diff --git a/tests/test_kern.py b/tests/test_kern.py index 1fb663dc..86cfb3f1 100644 --- a/tests/test_kern.py +++ b/tests/test_kern.py @@ -1,5 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ -This file contains test functions for KERN import and export. +This module contains test functions for KERN import and export. """ import unittest @@ -32,14 +34,14 @@ def test_examples(self): for fn in KERN_TESTFILES: part = merge_parts(load_kern(fn)) ka = ensure_notearray(part) - self.assertTrue(True == True) + self.assertTrue(True) def test_tie_mismatch(self): fn = KERN_TIES[0] part = merge_parts(load_kern(fn)) - self.assertTrue(True == True) + self.assertTrue(True) # if __name__ == "__main__": diff --git a/tests/test_key_estimation.py b/tests/test_key_estimation.py index d08f0339..1902901d 100644 --- a/tests/test_key_estimation.py +++ b/tests/test_key_estimation.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for the key estimation methods. +""" import unittest from partitura import EXAMPLE_MUSICXML diff --git a/tests/test_load_performance.py b/tests/test_load_performance.py index 313ed711..9319b561 100644 --- a/tests/test_load_performance.py +++ b/tests/test_load_performance.py @@ -1,7 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ - -This file contains test functions for the load_performance method - +This module contains test functions for the `load_performance` method """ import unittest @@ -10,7 +10,7 @@ from partitura import load_performance, EXAMPLE_MIDI from partitura.io import NotSupportedFormatError -from partitura.performance import PerformedPart, Performance +from partitura.performance import Performance class TestLoadScore(unittest.TestCase): diff --git a/tests/test_load_score.py b/tests/test_load_score.py index b6297217..e7b3c8f5 100644 --- a/tests/test_load_score.py +++ b/tests/test_load_score.py @@ -1,7 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ - -This file contains test functions for the load_score method - +This module contains test functions for the `load_score` method. """ import unittest diff --git a/tests/test_match_import.py b/tests/test_match_import.py index 0eb4df83..d92a93ae 100644 --- a/tests/test_match_import.py +++ b/tests/test_match_import.py @@ -1,17 +1,15 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ - -This file contains test functions for Matchfile import - +This module contains test functions for Matchfile import """ - -import logging import unittest -from tempfile import TemporaryFile +import numpy as np -from tests import MATCH_IMPORT_EXPORT_TESTFILES +from tests import MATCH_IMPORT_EXPORT_TESTFILES, MOZART_VARIATION_FILES from partitura.io.importmatch import MatchFile, parse_matchline -from partitura import load_match +from partitura import load_match, load_score, load_performance class TestLoadMatch(unittest.TestCase): @@ -62,13 +60,80 @@ def test_match_lines(self): mo = parse_matchline(ml) self.assertTrue(mo.matchline, ml) - # def test_load_match(self): - # for fn in MATCH_IMPORT_EXPORT_TESTFILES: + def test_load_match(self): + + perf_match, alignment, score_match = load_match( + filename=MOZART_VARIATION_FILES["match"], + create_score=True, + first_note_at_zero=True, + ) + + pna_match = perf_match.note_array() + sna_match = score_match.note_array() + + perf_midi = load_performance( + filename=MOZART_VARIATION_FILES["midi"], + first_note_at_zero=True, + ) + + pna_midi = perf_midi.note_array() + score_musicxml = load_score( + filename=MOZART_VARIATION_FILES["musicxml"], + ) + + sna_musicxml = score_musicxml.note_array() + + for note in alignment: + + # check score info in match and MusicXML + if "score_id" in note: + + idx_smatch = np.where(sna_match["id"] == note["score_id"])[0] + idx_sxml = np.where(sna_musicxml["id"] == note["score_id"])[0] + + self.assertTrue( + sna_match[idx_smatch]["pitch"] == sna_musicxml[idx_sxml]["pitch"] + ) + + self.assertTrue( + np.isclose( + sna_match[idx_smatch]["onset_beat"], + sna_match[idx_sxml]["onset_beat"], + ) + ) + + self.assertTrue( + np.isclose( + sna_match[idx_smatch]["duration_beat"], + sna_match[idx_sxml]["duration_beat"], + ) + ) + + # check performance info in match and MIDI + if "performance_id" in note: + + idx_pmatch = np.where(pna_match["id"] == note["performance_id"])[0] + idx_pmidi = np.where(pna_midi["id"] == note["performance_id"])[0] + + self.assertTrue( + pna_match[idx_pmatch]["pitch"] == pna_midi[idx_pmidi]["pitch"] + ) + + self.assertTrue( + np.isclose( + pna_match[idx_pmatch]["onset_sec"], + pna_match[idx_pmidi]["onset_sec"], + ) + ) - # # parse match file - # ppart, alignment, spart = load_match(fn, create_part=True) - # self.assertTrue(1, 1) + self.assertTrue( + np.isclose( + pna_match[idx_pmatch]["duration_sec"], + pna_match[idx_pmidi]["duration_sec"], + ) + ) + if __name__ == "__main__": diff --git a/tests/test_mei.py b/tests/test_mei.py index 0fd58b33..ae897083 100644 --- a/tests/test_mei.py +++ b/tests/test_mei.py @@ -1,5 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ -This file contains test functions for MEI export +This module contains test functions for MEI export """ import unittest diff --git a/tests/test_merge_parts.py b/tests/test_merge_parts.py index 70e486c8..5496e697 100644 --- a/tests/test_merge_parts.py +++ b/tests/test_merge_parts.py @@ -1,4 +1,8 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for the utilities for merging parts. +""" import numpy as np import logging import unittest diff --git a/tests/test_metrical_position.py b/tests/test_metrical_position.py index bdc6388e..4b4aecf2 100644 --- a/tests/test_metrical_position.py +++ b/tests/test_metrical_position.py @@ -1,7 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ - -This file contains test functions for the metrical position computation - +This module contains test functions for the metrical position computation """ import unittest diff --git a/tests/test_midi_export.py b/tests/test_midi_export.py index 1b088058..077f6f8a 100644 --- a/tests/test_midi_export.py +++ b/tests/test_midi_export.py @@ -1,5 +1,8 @@ #!/usr/bin/env python - +# -*- coding: utf-8 -*- +""" +This module cotains tests for exporting MIDI file methods. +""" import logging from collections import defaultdict, Counter, OrderedDict import unittest diff --git a/tests/test_midi_import.py b/tests/test_midi_import.py index a4de9dff..cf31f169 100644 --- a/tests/test_midi_import.py +++ b/tests/test_midi_import.py @@ -1,5 +1,8 @@ #!/usr/bin/env python - +# -*- coding: utf-8 -*- +""" +This module contains tests for importing MIDI files. +""" import logging from collections import defaultdict, Counter from operator import itemgetter diff --git a/tests/test_nakamura.py b/tests/test_nakamura.py index 998c0a28..cf6d9753 100644 --- a/tests/test_nakamura.py +++ b/tests/test_nakamura.py @@ -1,8 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ - -This file contains test functions for the import of Nakamura et al.'s match and +This module contains test functions for the import of Nakamura et al.'s match and corresp file formats. - """ import unittest diff --git a/tests/test_new_divs.py b/tests/test_new_divs.py index 6456ba1d..b0f828ee 100644 --- a/tests/test_new_divs.py +++ b/tests/test_new_divs.py @@ -1,5 +1,8 @@ #!/usr/bin/env python - +# -*- coding: utf-8 -*- +""" +This module contains tests for methods for handling divisions and time signatures. +""" import logging import unittest diff --git a/tests/test_note_array.py b/tests/test_note_array.py index c66fc336..c96c9ce8 100644 --- a/tests/test_note_array.py +++ b/tests/test_note_array.py @@ -1,7 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ This module contains the test cases for testing the note_array attribute of the Part class. - """ import unittest diff --git a/tests/test_note_features.py b/tests/test_note_features.py index b242563f..93004fa3 100644 --- a/tests/test_note_features.py +++ b/tests/test_note_features.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for methods for generating note-level features. +""" import unittest from tests import ( METRICAL_POSITION_TESTFILES, diff --git a/tests/test_parangonada.py b/tests/test_parangonada.py index 6c32ad92..b8f620e4 100644 --- a/tests/test_parangonada.py +++ b/tests/test_parangonada.py @@ -1,7 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ - -This file contains test functions for MusicXML import and export. - +This module contains test functions for Parangonada import and export. """ import logging diff --git a/tests/test_part_properties.py b/tests/test_part_properties.py index 6c774704..af3854bf 100644 --- a/tests/test_part_properties.py +++ b/tests/test_part_properties.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for part properties. +""" import unittest import partitura from partitura import score diff --git a/tests/test_performance.py b/tests/test_performance.py index f53609c0..2c864348 100644 --- a/tests/test_performance.py +++ b/tests/test_performance.py @@ -1,7 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ - -This file contains test functions for the `performance` module - +This module contains test functions for the `performance` module """ import unittest import numpy as np diff --git a/tests/test_performance_codec.py b/tests/test_performance_codec.py index 6c932237..6a23d373 100644 --- a/tests/test_performance_codec.py +++ b/tests/test_performance_codec.py @@ -1,7 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ - -This file contains test functions for Performance Array Calculations - +This module contains test functions for Performance Array Calculations """ import unittest import numpy as np diff --git a/tests/test_pianoroll.py b/tests/test_pianoroll.py index 23886e77..33361a34 100644 --- a/tests/test_pianoroll.py +++ b/tests/test_pianoroll.py @@ -1,4 +1,8 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for methods for computing piano rolls. +""" import numpy as np import logging import unittest diff --git a/tests/test_pitch_spelling.py b/tests/test_pitch_spelling.py index c8e34dd3..b509b3b9 100644 --- a/tests/test_pitch_spelling.py +++ b/tests/test_pitch_spelling.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for the pitch spelling algorithms. +""" import numpy as np import unittest diff --git a/tests/test_quarter_adjust.py b/tests/test_quarter_adjust.py index 49217daf..aa89789c 100644 --- a/tests/test_quarter_adjust.py +++ b/tests/test_quarter_adjust.py @@ -1,4 +1,8 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for adjusting quarter durations +""" import logging import unittest diff --git a/tests/test_rest_array.py b/tests/test_rest_array.py index d498f720..2bff16be 100644 --- a/tests/test_rest_array.py +++ b/tests/test_rest_array.py @@ -1,7 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ This module contains the test cases for testing the note_array attribute of the Part class. - """ import unittest diff --git a/tests/test_synth.py b/tests/test_synth.py index eb2bf152..62b10e99 100644 --- a/tests/test_synth.py +++ b/tests/test_synth.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for the synthesis methods. +""" import unittest import numpy as np @@ -17,10 +22,10 @@ from partitura import save_wav -RNG = np.random.RandomState(1984) - from tests import WAV_TESTFILES +RNG = np.random.RandomState(1984) + class TestMidiPitchToTemperedFrequency(unittest.TestCase): def test_octaves(self): @@ -28,14 +33,16 @@ def test_octaves(self): midi_pitch = np.arange(6) + 10 freq_ratios = [1.1, 1.2, 1.3, 1.44, 1.5, 1.55] # compute frequencies - frequency = midi_pitch_to_tempered_frequency(midi_pitch, - reference_midi_pitch = 10, - reference_frequency = 100, - interval_ratios = freq_ratios) + frequency = midi_pitch_to_tempered_frequency( + midi_pitch, + reference_midi_pitch=10, + reference_frequency=100, + interval_ratios=freq_ratios, + ) freq_ratios_new = frequency / 100 # make test self.assertTrue(np.allclose(freq_ratios, freq_ratios_new)) - + class TestMidiPitchToNaturalFrequency(unittest.TestCase): def test_octaves(self): diff --git a/tests/test_time_estimation.py b/tests/test_time_estimation.py index 52121bcd..d04cd44f 100644 --- a/tests/test_time_estimation.py +++ b/tests/test_time_estimation.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for the methods for estimating metrical information. +""" import numpy as np import unittest @@ -71,4 +76,4 @@ def testtempo(self): ) - \ No newline at end of file + diff --git a/tests/test_times.py b/tests/test_times.py index d1ac1371..2c25b130 100644 --- a/tests/test_times.py +++ b/tests/test_times.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for testing conversions from beats and quarters. +""" import unittest import partitura.score as score diff --git a/tests/test_tonal_tension.py b/tests/test_tonal_tension.py index 18c15843..b7a6f970 100644 --- a/tests/test_tonal_tension.py +++ b/tests/test_tonal_tension.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for the methods computing tonal tension. +""" import unittest from partitura import ( diff --git a/tests/test_utils.py b/tests/test_utils.py index 44185eeb..c2ce7589 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for the utility methods. +""" import unittest import partitura import numpy as np diff --git a/tests/test_voice_estimation.py b/tests/test_voice_estimation.py index 0128f0d5..5c8716eb 100644 --- a/tests/test_voice_estimation.py +++ b/tests/test_voice_estimation.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module contains tests for the voice estimation methods. +""" import numpy as np import unittest diff --git a/tests/test_xml.py b/tests/test_xml.py index 1df7cf47..8323fb54 100755 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -1,7 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- """ - -This file contains test functions for MusicXML import and export. - +This module contains test functions for MusicXML import and export. """ import logging From 47ffe619b79ebdc1362f8b0bc10cea862300cb99 Mon Sep 17 00:00:00 2001 From: sildater <41552783+sildater@users.noreply.github.com> Date: Tue, 25 Oct 2022 10:51:48 +0200 Subject: [PATCH 35/41] add a 5-limit temperament table --- partitura/utils/synth.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/partitura/utils/synth.py b/partitura/utils/synth.py index 9de84566..879e9ace 100644 --- a/partitura/utils/synth.py +++ b/partitura/utils/synth.py @@ -40,6 +40,23 @@ 12: 2, } +# symmetric five limit temperament with supertonic = 10:9 +FIVE_LIMIT_INTERVAL_RATIOS = { + 0: 1, + 1: 16 / 15, + 2: 10 / 9, + 3: 6 / 5, + 4: 5 / 4, + 5: 4 / 3, + 6: 7 / 5, + 7: 3 / 2, + 8: 8 / 5, + 9: 5 / 3, + 10: 9 / 5, + 11: 15 / 8, + 12: 2 +} + def midi_pitch_to_natural_frequency( midi_pitch: Union[int, float, np.ndarray], @@ -103,7 +120,7 @@ def midi_pitch_to_tempered_frequency( midi_pitch: Union[int, float, np.ndarray], reference_midi_pitch: Union[int, float] = 69, reference_frequency: float = A4, - interval_ratios: Dict[int, float] = NATURAL_INTERVAL_RATIOS, + interval_ratios: Dict[int, float] = FIVE_LIMIT_INTERVAL_RATIOS, ) -> Union[float, np.ndarray]: """ Convert MIDI pitch to frequency in Hz using From 0d09d4fe804ffb55edf88ccb86e198a900d9db5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Tue, 25 Oct 2022 17:32:04 +0200 Subject: [PATCH 36/41] use midi_pitch_to_tempered_frequency and update documentation --- partitura/io/exportaudio.py | 22 +++++++++++++--------- partitura/utils/synth.py | 26 +++++++++++++++++++------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/partitura/io/exportaudio.py b/partitura/io/exportaudio.py index eeeb5dbc..524ed480 100644 --- a/partitura/io/exportaudio.py +++ b/partitura/io/exportaudio.py @@ -46,16 +46,20 @@ def save_wav( If "linear" or "exp", the methods `lin_in_lin_out` and `exp_in_exp_out` in `partitura.utils.synth` will be used. Otherwise this argument should be a callable. See `lin_in_lin_out` for more details. - tuning: {"equal_temperament", "natural"} or callable - Method for tuning. If "equal temperament" will use equally spaced - semitones, while "natural" will use natural ratios within an octave. - Otherwise it uses a callable. See `midi_pitch_to_natural_frequency` - and `midi_pitch_to_frequency` for more info on the "equal_temperament" - and "natural" tuning. + tuning: {"equal_temperament", "natural"} or callable. + The tuning system to use. If the value is "equal_temperament", + 12 tone equal temperament implemented in `midi_pitch_to_frequency` will + be used. If the value is "natural", the function + `midi_pitch_to_tempered_frequency` will be used. Note that + `midi_pitch_to_tempered_frequency` computes the intervals (and thus, + frequencies) with respect to a reference note (A4 by default). See + the documentation of this function for more information. If a callable + is provided, function should get MIDI pitch as input and return + frequency in Hz as output. tuning_kwargs : dict - Keyword arguments to be passed to the tuning method selected in `tuning`. See - `midi_pitch_to_natural_frequency` and `midi_pitch_to_frequency` - for more info on the "equal_temperament" and "natural" tuning. + Dictionary of keyword arguments to be passed to the tuning function + specified in `tuning`. See `midi_pitch_to_tempered_frequency` and + `midi_pitch_to_frequency` for more information on their keyword arguments. harmonic_dist : int, "shepard" or None (optional) Distribution of harmonics. If an integer, it is the number of harmonics to be considered. If "shepard", it uses Shepard tones. diff --git a/partitura/utils/synth.py b/partitura/utils/synth.py index 879e9ace..2f7d0366 100644 --- a/partitura/utils/synth.py +++ b/partitura/utils/synth.py @@ -43,16 +43,16 @@ # symmetric five limit temperament with supertonic = 10:9 FIVE_LIMIT_INTERVAL_RATIOS = { 0: 1, - 1: 16 / 15, + 1: 16 / 15, 2: 10 / 9, - 3: 6 / 5, + 3: 6 / 5, 4: 5 / 4, 5: 4 / 3, - 6: 7 / 5, + 6: 7 / 5, 7: 3 / 2, 8: 8 / 5, 9: 5 / 3, - 10: 9 / 5, + 10: 9 / 5, 11: 15 / 8, 12: 2 } @@ -349,8 +349,20 @@ def synthesize( The sample rate of the audio file in Hz. envelope_fun: {"linear", "exp" } The type of envelop to apply to the individual sine waves. - tuning: {"equal_temperament", "natural"} - The tuning system to use. + tuning: {"equal_temperament", "natural"} or callable. + The tuning system to use. If the value is "equal_temperament", + 12 tone equal temperament implemented in `midi_pitch_to_frequency` will + be used. If the value is "natural", the function + `midi_pitch_to_tempered_frequency` will be used. Note that + `midi_pitch_to_tempered_frequency` computes the intervals (and thus, + frequencies) with respect to a reference note (A4 by default). See + the documentation of this function for more information. If a callable + is provided, function should get MIDI pitch as input and return + frequency in Hz as output. + tuning_kwargs : dict + Dictionary of keyword arguments to be passed to the tuning function + specified in `tuning`. See `midi_pitch_to_tempered_frequency` and + `midi_pitch_to_frequency` for more information on their keyword arguments. harmonic_dist : int, "shepard" or None (optional) Distribution of harmonics. If an integer, it is the number of harmonics to be considered. If "shepard", it uses Shepard tones. @@ -407,7 +419,7 @@ def synthesize( if tuning == "equal_temperament": freq_in_hz = midi_pitch_to_frequency(pitch, **tuning_kwargs) elif tuning == "natural": - freq_in_hz = midi_pitch_to_natural_frequency(pitch, **tuning_kwargs) + freq_in_hz = midi_pitch_to_tempered_frequency(pitch, **tuning_kwargs) elif callable(tuning): freq_in_hz = tuning(pitch, **tuning_kwargs) From 2e3fe8858b1c806024a411f96c6625e27261ceb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cancino-Chac=C3=B3n?= Date: Tue, 25 Oct 2022 17:39:14 +0200 Subject: [PATCH 37/41] update docstrings --- partitura/io/exportaudio.py | 17 +++++++++-------- partitura/utils/synth.py | 5 +++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/partitura/io/exportaudio.py b/partitura/io/exportaudio.py index 524ed480..c41c17f9 100644 --- a/partitura/io/exportaudio.py +++ b/partitura/io/exportaudio.py @@ -48,14 +48,15 @@ def save_wav( be a callable. See `lin_in_lin_out` for more details. tuning: {"equal_temperament", "natural"} or callable. The tuning system to use. If the value is "equal_temperament", - 12 tone equal temperament implemented in `midi_pitch_to_frequency` will - be used. If the value is "natural", the function - `midi_pitch_to_tempered_frequency` will be used. Note that - `midi_pitch_to_tempered_frequency` computes the intervals (and thus, - frequencies) with respect to a reference note (A4 by default). See - the documentation of this function for more information. If a callable - is provided, function should get MIDI pitch as input and return - frequency in Hz as output. + 12 tone equal temperament implemented in + `partitura.utils.music.midi_pitch_to_frequency` will be used. If the value is + "natural", the function `partitura.utils.synth.midi_pitch_to_tempered_frequency` + will be used. Note that `midi_pitch_to_tempered_frequency` computes + intervals (and thus, frequencies) with respect to a reference note + (A4 by default) and uses the interval ratios specified by + `partitura.utils.synth.FIVE_LIMIT_INTERVAL_RATIOS`. See the documentation of + these functions for more information. If a callable is provided, function should + get MIDI pitch as input and return frequency in Hz as output. tuning_kwargs : dict Dictionary of keyword arguments to be passed to the tuning function specified in `tuning`. See `midi_pitch_to_tempered_frequency` and diff --git a/partitura/utils/synth.py b/partitura/utils/synth.py index 2f7d0366..785494e6 100644 --- a/partitura/utils/synth.py +++ b/partitura/utils/synth.py @@ -355,8 +355,9 @@ def synthesize( be used. If the value is "natural", the function `midi_pitch_to_tempered_frequency` will be used. Note that `midi_pitch_to_tempered_frequency` computes the intervals (and thus, - frequencies) with respect to a reference note (A4 by default). See - the documentation of this function for more information. If a callable + frequencies) with respect to a reference note (A4 by default) and uses the + interval ratios specified by `FIVE_LIMIT_INTERVAL_RATIOS`. See + the documentation of these functions for more information. If a callable is provided, function should get MIDI pitch as input and return frequency in Hz as output. tuning_kwargs : dict From 8256763388b2d4e9bb9483cb407813dc6d055075 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 29 Oct 2022 14:26:39 +0200 Subject: [PATCH 38/41] updated release No. to 1.1.1 --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 1f43c771..4236e434 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -29,9 +29,9 @@ # built documents. # # The short X.Y version. -version = "1.1.0" # pkg_resources.get_distribution("partitura").version +version = "1.1.1" # pkg_resources.get_distribution("partitura").version # The full version, including alpha/beta/rc tags. -release = "1.1.0" +release = "1.1.1" # # The full version, including alpha/beta/rc tags # release = pkg_resources.get_distribution("partitura").version diff --git a/setup.py b/setup.py index d44fb762..e68a70aa 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ EMAIL = "partitura-users@googlegroups.com" AUTHOR = "Maarten Grachten, Carlos Cancino-Chacón, Silvan Peter, Emmanouil Karystinaios, Francesco Foscarin, Thassilo Gadermaier" REQUIRES_PYTHON = ">=3.6" -VERSION = "1.1.0" +VERSION = "1.1.1" # What packages are required for this module to be executed? REQUIRED = ["numpy", "scipy", "lxml", "lark-parser", "xmlschema", "mido"] From 52b47d411b449d3085c1dd1735dcae262b99be56 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Sat, 29 Oct 2022 14:28:29 +0200 Subject: [PATCH 39/41] Updated changes record to include release 1.1.1 bug fixes. --- CHANGES.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index d83d80c5..add2f593 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,28 @@ Release Notes ============= + +Version 1.1.1 (Released on 2022-10-31) +-------------------------------------- + +New features: + +* New minor feature : Adding midi pitch to freq for synthesizer add reference-based midi pitch to freq #163 + +Bug fixes: + +* Documentation Fix of ReadTheDocs +* Bug fix Bug synthesizing scores with pickup measures #166 Synthesizing score with pick up measure +* Bug Fix of kern import Kern import fix #160 +* Bug Fix of Musicxml import repeat infer Bug with musicxml Import #161 +* Bug fix Note array with empty voice Note array from note list with empty voice bug. #159 +* Fix synthesizing scores with pickup measures #167 + +Other changes: + +* Encoding declaration on all files. + + Version 1.0.0 (Released on 2022-09-20) -------------------------------------- From c34ab54c34c8443a5850b111038285098cf50881 Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Fri, 4 Nov 2022 17:34:53 +0100 Subject: [PATCH 40/41] updated workflow. --- .github/workflows/partitura_unittests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/partitura_unittests.yml b/.github/workflows/partitura_unittests.yml index 22eeedc0..1b75dcce 100644 --- a/.github/workflows/partitura_unittests.yml +++ b/.github/workflows/partitura_unittests.yml @@ -2,9 +2,9 @@ name: Partitura Unittests on: push: - branches: [master, develop] + branches: [main, develop] pull_request: - branches: [master, develop] + branches: [develop] jobs: test: From f86320ca4fd2369be16ebfe01c93f710d116766d Mon Sep 17 00:00:00 2001 From: Francesco Foscarin Date: Fri, 4 Nov 2022 17:38:39 +0100 Subject: [PATCH 41/41] Update CHANGES.md added branch renaming on changes.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index add2f593..df5b95c6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ Bug fixes: Other changes: * Encoding declaration on all files. +* Renaming master branch as main Version 1.0.0 (Released on 2022-09-20)