├── tests ├── __init__.py ├── test_midi_mapping.py ├── fixtures.py ├── test_descriptors.py └── test_init.py ├── midi ├── boska │ ├── 1.mid │ ├── 2.mid │ ├── 3.mid │ ├── 4.mid │ ├── 5.mid │ ├── 6.mid │ ├── 7.mid │ ├── 8.mid │ ├── 9.mid │ └── 10.mid ├── sano │ ├── 1.mid │ ├── 10.mid │ ├── 2.mid │ ├── 3.mid │ ├── 4.mid │ ├── 5.mid │ ├── 6.mid │ ├── 7.mid │ ├── 8.mid │ └── 9.mid └── two_bar │ ├── four_kicks.mid │ └── unbalanced_1.mid ├── .gitignore ├── setup.cfg ├── setup.py ├── .github └── workflows │ └── run_tests.yml ├── README.md └── rhythmtoolbox ├── midi_mapping.py ├── __init__.py └── descriptors.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /midi/boska/1.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/boska/1.mid -------------------------------------------------------------------------------- /midi/boska/2.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/boska/2.mid -------------------------------------------------------------------------------- /midi/boska/3.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/boska/3.mid -------------------------------------------------------------------------------- /midi/boska/4.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/boska/4.mid -------------------------------------------------------------------------------- /midi/boska/5.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/boska/5.mid -------------------------------------------------------------------------------- /midi/boska/6.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/boska/6.mid -------------------------------------------------------------------------------- /midi/boska/7.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/boska/7.mid -------------------------------------------------------------------------------- /midi/boska/8.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/boska/8.mid -------------------------------------------------------------------------------- /midi/boska/9.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/boska/9.mid -------------------------------------------------------------------------------- /midi/sano/1.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/sano/1.mid -------------------------------------------------------------------------------- /midi/sano/10.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/sano/10.mid -------------------------------------------------------------------------------- /midi/sano/2.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/sano/2.mid -------------------------------------------------------------------------------- /midi/sano/3.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/sano/3.mid -------------------------------------------------------------------------------- /midi/sano/4.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/sano/4.mid -------------------------------------------------------------------------------- /midi/sano/5.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/sano/5.mid -------------------------------------------------------------------------------- /midi/sano/6.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/sano/6.mid -------------------------------------------------------------------------------- /midi/sano/7.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/sano/7.mid -------------------------------------------------------------------------------- /midi/sano/8.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/sano/8.mid -------------------------------------------------------------------------------- /midi/sano/9.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/sano/9.mid -------------------------------------------------------------------------------- /midi/boska/10.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/boska/10.mid -------------------------------------------------------------------------------- /midi/two_bar/four_kicks.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/two_bar/four_kicks.mid -------------------------------------------------------------------------------- /midi/two_bar/unbalanced_1.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielgomezmarin/rhythmtoolbox/HEAD/midi/two_bar/unbalanced_1.mid -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .idea/ 3 | __pycache__/ 4 | .pytest_cache/ 5 | *.egg-info/ 6 | tests/results/ 7 | .DS_Store 8 | *.pyc 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths = tests 3 | addopts = -v --cov --cov-report html:tests/results/cov_html 4 | 5 | [coverage:run] 6 | omit = tests/* 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | test_requires = ["pytest", "pytest-cov"] 4 | 5 | setup( 6 | name="rhythmtoolbox", 7 | author="danielgomezmarin", 8 | version="0.1.0", 9 | url="https://github.com/danielgomezmarin/rhythmtoolbox", 10 | packages=find_packages( 11 | exclude=[ 12 | "tests*", 13 | ] 14 | ), 15 | install_requires=["numpy~=1.24.2", "pretty_midi~=0.2.10", "scipy~=1.10.1"], 16 | extras_require={ 17 | "test": test_requires, 18 | }, 19 | ) 20 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: run_tests 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python 3.11 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: 3.11 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | python -m pip install -e .[test] 21 | - name: Test with pytest 22 | run: | 23 | pytest -v --cov 24 | -------------------------------------------------------------------------------- /tests/test_midi_mapping.py: -------------------------------------------------------------------------------- 1 | from .fixtures import BOSKA_3, BOSKA_8, BOSKA_9 2 | 3 | from rhythmtoolbox.midi_mapping import event_to_3number, event_to_8number, get_band 4 | 5 | 6 | def test_get_band(): 7 | # fmt: off 8 | assert get_band(BOSKA_3, "low").tolist() == [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0] 9 | assert get_band(BOSKA_3, "mid").tolist() == [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1] 10 | assert get_band(BOSKA_3, "hi").tolist() == [1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0] 11 | 12 | assert get_band(BOSKA_8, "low").tolist() == [0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0] 13 | assert get_band(BOSKA_8, "mid").tolist() == [1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1] 14 | assert get_band(BOSKA_8, "hi").tolist() == [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0] 15 | 16 | assert get_band(BOSKA_9, "low").tolist() == [1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0] 17 | assert get_band(BOSKA_9, "mid").tolist() == [0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0] 18 | assert get_band(BOSKA_9, "hi").tolist() == [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0] 19 | # fmt: on 20 | 21 | 22 | def test_event_to_8number(): 23 | assert event_to_8number([36, 38, 46]) == [1, 2, 4] 24 | assert event_to_8number([37, 38, 39, 42, 46]) == [2, 3, 4, 5, 6] 25 | 26 | 27 | def test_event_to_3number(): 28 | assert event_to_3number([36, 38, 46]) == [1, 2, 3] 29 | assert event_to_3number([37, 38, 39, 42, 46]) == [2, 3] 30 | -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | from rhythmtoolbox import pattlist_to_pianoroll 2 | 3 | # TODO: add test case for non-16-step pattern 4 | PATT_1 = [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0] 5 | PATT_2 = [1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0] 6 | 7 | BOSKA_3_PATTLIST = [ 8 | [36, 38, 42], 9 | [], 10 | [42], 11 | [38, 42], 12 | [46], 13 | [], 14 | [36, 38, 42], 15 | [], 16 | [42], 17 | [38], 18 | [36, 42], 19 | [], 20 | [38, 46], 21 | [], 22 | [38, 42, 64], 23 | [38], 24 | ] 25 | 26 | BOSKA_3 = pattlist_to_pianoroll(BOSKA_3_PATTLIST) 27 | 28 | BOSKA_3_DESCRIPTORS = { 29 | "noi": 5, 30 | "polyDensity": 20, 31 | "lowDensity": 4, 32 | "midDensity": 7, 33 | "hiDensity": 9, 34 | "lowness": 0.36363636363636365, 35 | "midness": 0.6363636363636364, 36 | "hiness": 0.8181818181818182, 37 | "stepDensity": 0.6875, 38 | "sync": -10, 39 | "lowSync": -7, 40 | "midSync": -4, 41 | "hiSync": -14, 42 | "syness": -0.9090909090909091, 43 | "lowSyness": -1.75, 44 | "midSyness": -0.5714285714285714, 45 | "hiSyness": -1.5555555555555556, 46 | "balance": 0.9623442216024459, 47 | "polyBalance": 0.8621714780149552, 48 | "evenness": 0.9841710008484362, 49 | "polyEvenness": 4.818647489725106, 50 | "polySync": 13, 51 | } 52 | 53 | BOSKA_8_PATTLIST = [ 54 | [38, 39, 46], 55 | [], 56 | [37], 57 | [36], 58 | [], 59 | [], 60 | [38, 39, 46], 61 | [], 62 | [37], 63 | [36], 64 | [], 65 | [], 66 | [38, 39, 46], 67 | [38, 39, 46], 68 | [37], 69 | [38], 70 | ] 71 | 72 | BOSKA_8 = pattlist_to_pianoroll(BOSKA_8_PATTLIST) 73 | 74 | BOSKA_8_DESCRIPTORS = { 75 | "noi": 5, 76 | "polyDensity": 14, 77 | "lowDensity": 2, 78 | "midDensity": 8, 79 | "hiDensity": 4, 80 | "lowness": 0.2, 81 | "midness": 0.8, 82 | "hiness": 0.4, 83 | "stepDensity": 0.625, 84 | "sync": -2, 85 | "lowSync": 3, 86 | "midSync": -9, 87 | "hiSync": -4, 88 | "syness": -0.2, 89 | "lowSyness": 1.5, 90 | "midSyness": -1.125, 91 | "hiSyness": -1.0, 92 | "balance": 0.8186690031367755, 93 | "polyBalance": 0.7887402430901225, 94 | "evenness": 0.8820076387881416, 95 | "polyEvenness": 4.575377577313285, 96 | "polySync": 0, 97 | } 98 | 99 | BOSKA_9_PATTLIST = [ 100 | [36, 42], 101 | [], 102 | [42], 103 | [], 104 | [42], 105 | [], 106 | [36, 38, 46], 107 | [], 108 | [37, 42], 109 | [36, 38], 110 | [], 111 | [37], 112 | [36, 38, 42], 113 | [], 114 | [37, 38, 39, 42, 46], 115 | [], 116 | ] 117 | 118 | BOSKA_9 = pattlist_to_pianoroll(BOSKA_9_PATTLIST) 119 | 120 | BOSKA_9_DESCRIPTORS = { 121 | "noi": 6, 122 | "polyDensity": 17, 123 | "lowDensity": 4, 124 | "midDensity": 6, 125 | "hiDensity": 7, 126 | "lowness": 0.4444444444444444, 127 | "midness": 0.6666666666666666, 128 | "hiness": 0.7777777777777778, 129 | "stepDensity": 0.5625, 130 | "sync": -10, 131 | "lowSync": -6, 132 | "midSync": -3, 133 | "hiSync": -14, 134 | "syness": -1.1111111111111112, 135 | "lowSyness": -1.5, 136 | "midSyness": -0.5, 137 | "hiSyness": -2.0, 138 | "balance": 0.905804548330825, 139 | "polyBalance": 0.7646388055112554, 140 | "evenness": 0.9842351591376168, 141 | "polyEvenness": 4.930845709389592, 142 | "polySync": 3, 143 | } 144 | 145 | # TODO: store file descriptors in files 146 | # midi/two_bar/four_kicks.mid 147 | FOUR_KICKS_DESCRIPTORS = { 148 | "noi": 1, 149 | "polyDensity": 4, 150 | "lowDensity": 4, 151 | "midDensity": 0, 152 | "hiDensity": 0, 153 | "lowness": 1.0, 154 | "midness": 0.0, 155 | "hiness": 0.0, 156 | "stepDensity": 0.125, 157 | "sync": -7.0, 158 | "lowSync": -7.0, 159 | "midSync": 0.0, 160 | "hiSync": 0.0, 161 | "syness": -3.5, 162 | "lowSyness": -3.5, 163 | "midSyness": 0.0, 164 | "hiSyness": 0.0, 165 | "balance": 1, 166 | "polyBalance": 1, 167 | "evenness": 1.0, 168 | "polyEvenness": 3.0, 169 | "polySync": 0.0, 170 | } 171 | 172 | # midi/two_bar/unbalanced_1.mid 173 | UNBALANCED_1_DESCRIPTORS = { 174 | "noi": 2, 175 | "polyDensity": 7, 176 | "lowDensity": 3, 177 | "midDensity": 4, 178 | "hiDensity": 0, 179 | "lowness": 0.42857142857142855, 180 | "midness": 0.5714285714285714, 181 | "hiness": 0.0, 182 | "stepDensity": 0.21875, 183 | "sync": -3.0, 184 | "lowSync": -3.0, 185 | "midSync": -1.0, 186 | "hiSync": 0.0, 187 | "syness": -0.5416666666666666, 188 | "lowSyness": -1.25, 189 | "midSyness": -0.5, 190 | "hiSyness": 0.0, 191 | "balance": 0.5581811742566036, 192 | "polyBalance": 0.5503215251637058, 193 | "evenness": 0.8536618422656435, 194 | "polyEvenness": 4.691341716182545, 195 | "polySync": 0.0, 196 | } 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rhythm Toolbox 2 | 3 | This repository contains tools for studying rhythms in symbolic format. It was developed for the study of polyphonic 4 | drum patterns, but can be adapted for other types of patterns. It implements various descriptors derived from scientific 5 | literature; see [Descriptors](#descriptors) for a full list with references. 6 | 7 | ## Installation 8 | 9 | ``` 10 | pip install git+https://github.com/danielgomezmarin/rhythmtoolbox 11 | ``` 12 | 13 | ## Usage 14 | 15 | Rhythm Toolbox supports multiple representations of symbolic rhythm. Across all representations, Rhythm Toolbox operates 16 | at a 16th note resolution, or 4 ticks per beat in MIDI terms. If data is passed in at a different resolution, it is 17 | resampled by associating each onset with its closest 16th note position. 18 | 19 | #### MIDI 20 | 21 | To compute descriptors from a MIDI file: 22 | 23 | ```python 24 | from rhythmtoolbox import midifile2descriptors 25 | 26 | midifile2descriptors('midi/boska/3.mid') 27 | ``` 28 | 29 | #### Piano roll 30 | 31 | A [piano roll](https://en.wikipedia.org/wiki/Piano_roll#In_digital_audio_workstations) is a `(N, V)` matrix, where `N` 32 | is a number of time steps and `V` is a number of MIDI pitches. Any positive value represents an onset; Rhythm Toolbox 33 | does not currently consider velocity. 34 | 35 | To compute descriptors from a piano roll: 36 | 37 | ```python 38 | from rhythmtoolbox import pianoroll2descriptors 39 | 40 | pianoroll2descriptors(roll) 41 | ``` 42 | 43 | #### Pattern list 44 | 45 | A pattern list is a list of lists representing time steps, each containing the MIDI note numbers that occur at that 46 | step. 47 | 48 | To compute descriptors from a pattern list: 49 | 50 | ```python 51 | from rhythmtoolbox import pattlist2descriptors 52 | 53 | pattlist = [ 54 | [36, 38, 42], 55 | [], 56 | [], 57 | [38, 42], 58 | [46], 59 | [46], 60 | [36, 38, 42], 61 | [], 62 | [42], 63 | [38], 64 | [36, 42], 65 | [], 66 | [38, 46], 67 | [46], 68 | [42, 64], 69 | [], 70 | ] 71 | pattlist2descriptors(pattlist) 72 | ``` 73 | 74 | ## Descriptors 75 | 76 | The following descriptors are discussed in [Gómez-Marín et al, 2020](https://doi.org/10.1080/09298215.2020.1806887). 77 | Additional sources are listed where applicable. The mapping of MIDI instruments to frequency bands can be found 78 | in [midi_mapping.py](./rhythmtoolbox/midi_mapping.py). 79 | 80 | | Name | Description | 81 | |-------------|------------------------------------------| 82 | | noi | Number of instruments | 83 | | polyDensity | Number of onsets | 84 | | lowDensity | Number of onsets in the low freq band | 85 | | midDensity | Number of onsets in the mid freq band | 86 | | hiDensity | Number of onsets in the high freq band | 87 | | lowness | Fraction of onsets in the low freq band | 88 | | midness | Fraction of onsets in the mid freq band | 89 | | hiness | Fraction of onsets in the high freq band | 90 | | stepDensity | Fraction of steps with onsets | 91 | 92 | The following descriptors are valid only for 16-step patterns: 93 | 94 | | Name | Description | Reference | 95 | |--------------|---------------------------------------------|--------------------------------------------------------------------------| 96 | | sync | Syncopation | | 97 | | lowSync | Syncopation of the low freq band | | 98 | | midSync | Syncopation of the mid freq band | | 99 | | hiSync | Syncopation of the high freq band | | 100 | | syness | Syncopation / density | | 101 | | lowSyness | Syncopation / density of the low freq band | | 102 | | midSyness | Syncopation / density of the mid freq band | | 103 | | hiSyness | Syncopation / density of the high freq band | | 104 | | balance | Monophonic balance | [Milne and Herff, 2020](https://doi.org/10.1016/j.cognition.2020.104233) | 105 | | polyBalance | Polyphonic balance | | 106 | | evenness | Monophonic evenness | [Milne and Dean, 2016](https://doi.org/10.1162/COMJ_a_00343) | 107 | | polyEvenness | Polyphonic evenness | | 108 | | polySync | Polyphonic syncopation | [Witek et al, 2014](https://doi.org/10.
1371/journal.pone.0094446) | 109 | 110 | ## Attribution 111 | 112 | If this repository is useful for your research please cite the 113 | following [paper](https://doi.org/10.1080/09298215.2020.1806887) 114 | via the BibTeX below. 115 | 116 | @article{gomez2020drum, 117 | title={Drum rhythm spaces: From polyphonic similarity to generative maps}, 118 | author={G{\'o}mez-Mar{\'\i}n, Daniel and Jord{\`a}, Sergi and Herrera, Perfecto}, 119 | journal={Journal of New Music Research}, 120 | volume={49}, 121 | number={5}, 122 | pages={438--456}, 123 | year={2020}, 124 | publisher={Taylor \& Francis} 125 | } 126 | 127 | To cite the initial version of this repository (Nov 2018), checkout 128 | commit [`6acdb69a60153d08`](https://github.com/danielgomezmarin/rhythmtoolbox/tree/6acdb69a60153d0874da87560df3d7c62765e27a). 129 | 130 | The MIDI drum patterns examples included in [midi/boska](./midi/boska) and [midi/sano](./midi/sano) were provided by 131 | Jon-Eirik Boska and Sebastián Hoyos, respectively. 132 | -------------------------------------------------------------------------------- /rhythmtoolbox/midi_mapping.py: -------------------------------------------------------------------------------- 1 | """A mapping of the General MIDI Percussion Key Map (GMPKM) to three frequency levels: low, mid, and high""" 2 | 3 | low_instruments = [35, 36, 41, 45, 47, 64] 4 | mid_instruments = [37, 38, 39, 40, 43, 48, 50, 58, 61, 62, 65, 77] 5 | hi_instruments = [ 6 | 22, 7 | 26, 8 | 42, 9 | 44, 10 | 46, 11 | 49, 12 | 51, 13 | 52, 14 | 53, 15 | 54, 16 | 55, 17 | 56, 18 | 57, 19 | 59, 20 | 60, 21 | 62, 22 | 69, 23 | 70, 24 | 71, 25 | 72, 26 | 76, 27 | ] 28 | 29 | GM_dict = { 30 | # key is midi note number 31 | # values are: 32 | # [0] name (as string) 33 | # [1] name category low mid or high (as string) 34 | # [2] substiture midi number for simplified MIDI (all instruments) 35 | # [3] name of instrument for 8 note conversion (as string) 36 | # [4] number of instrument for 8 note conversion 37 | # [5] substiture midi number for conversion to 8 note 38 | # [6] substiture midi number for conversion to 16 note 39 | # [7] substiture midi number for conversion to 3 note 40 | # if we are going to remap just use GM_dict[msg.note][X] 41 | 22: ["Closed Hi-Hat edge", "high", 42, "CH", 3, 42, 42, 42], 42 | 26: ["Open Hi-Hat edge", "high", 46, "OH", 4, 46, 46, 42], 43 | 35: ["Acoustic Bass Drum", "low", 36, "K", 1, 36, 36, 36], 44 | 36: ["Bass Drum 1", "low", 36, "K", 1, 36, 36, 36], 45 | 37: ["Side Stick", "mid", 37, "RS", 6, 37, 37, 38], 46 | 38: ["Acoustic Snare", "mid", 38, "SN", 2, 38, 38, 38], 47 | 39: ["Hand Clap", "mid", 39, "CP", 5, 39, 39, 38], 48 | 40: ["Electric Snare", "mid", 38, "SN", 2, 38, 38, 38], 49 | 41: ["Low Floor Tom", "low", 45, "LT", 7, 45, 45, 36], 50 | 42: ["Closed Hi Hat", "high", 42, "CH", 3, 42, 42, 42], 51 | 43: ["High Floor Tom", "mid", 45, "HT", 8, 45, 45, 38], 52 | 44: ["Pedal Hi-Hat", "high", 46, "OH", 4, 46, 46, 42], 53 | 45: ["Low Tom", "low", 45, "LT", 7, 45, 45, 36], 54 | 46: ["Open Hi-Hat", "high", 46, "OH", 4, 46, 46, 42], 55 | 47: ["Low-Mid Tom", "low", 47, "MT", 7, 45, 47, 36], 56 | 48: ["Hi-Mid Tom", "mid", 47, "MT", 7, 50, 50, 38], 57 | 49: ["Crash Cymbal 1", "high", 49, "CC", 4, 46, 42, 42], 58 | 50: ["High Tom", "mid", 50, "HT", 8, 50, 50, 38], 59 | 51: ["Ride Cymbal 1", "high", 51, "RC", -1, 42, 51, 42], 60 | 52: ["Chinese Cymbal", "high", 52, "", -1, 46, 51, 42], 61 | 53: ["Ride Bell", "high", 53, "", -1, 42, 51, 42], 62 | 54: ["Tambourine", "high", 54, "", -1, 42, 69, 42], 63 | 55: ["Splash Cymbal", "high", 55, "OH", 4, 46, 42, 42], 64 | 56: ["Cowbell", "high", 56, "CB", -1, 37, 56, 42], 65 | 57: ["Crash Cymbal 2", "high", 57, "CC", 4, 46, 42, 42], 66 | 58: ["Vibraslap", "mid", 58, "VS", 6, 37, 37, 42], 67 | 59: ["Ride Cymbal 2", "high", 59, "RC", 3, 42, 51, 42], 68 | 60: ["Hi Bongo", "high", 60, "LB", 8, 45, 63, 42], 69 | 61: ["Low Bongo", "mid", 61, "HB", 7, 45, 64, 38], 70 | 62: ["Mute Hi Conga", "mid", 62, "MC", 8, 50, 62, 38], 71 | 63: ["Open Hi Conga", "high", 63, "HC", 8, 50, 63, 42], 72 | 64: ["Low Conga", "low", 64, "LC", 7, 45, 64, 36], 73 | 65: ["High Timbale", "mid", 65, "", 8, 45, 63, 38], 74 | 66: ["Low Timbale", "low", 66, "", 7, 45, 64, 36], 75 | 67: ["High Agogo", "high", 67, "", -1, 37, 56, 42], 76 | 68: ["Low Agogo", "mid", 68, "", -1, 37, 56, 38], 77 | 69: ["Cabasa", "high", 69, "MA", -1, 42, 69, 42], 78 | 70: ["Maracas", "high", 69, "MA", -1, 42, 69, 42], 79 | 71: ["Short Whistle", "high", 71, "", -1, 37, 56, 42], 80 | 72: ["Long Whistle", "high", 72, "", -1, 37, 56, 42], 81 | 73: ["Short Guiro", "high", 73, "", -1, 42, 42, 42], 82 | 74: ["Long Guiro", "high", 74, "", -1, 46, 46, 42], 83 | 75: ["Claves", "high", 75, "", -1, 37, 75, 42], 84 | 76: ["Hi Wood Block", "high", 76, "", 8, 50, 63, 42], 85 | 77: ["Low Wood Block", "mid", 77, "", 7, 45, 64, 38], 86 | 78: ["Mute Cuica", "high", 78, "", -1, 50, 62, 42], 87 | 79: ["Open Cuica", "high", 79, "", -1, 45, 63, 42], 88 | 80: ["Mute Triangle", "high", 80, "", -1, 37, 75, 42], 89 | 81: ["Open Triangle", "high", 81, "", -1, 37, 75, 42], 90 | } 91 | 92 | 93 | def get_band(roll, band="low"): 94 | """Returns a monophonic onset pattern of instruments in the given frequency band. 95 | 96 | roll, np.array 97 | Piano roll 98 | 99 | band, str 100 | "low", "mid", or "hi" 101 | """ 102 | range_map = { 103 | "low": low_instruments, 104 | "mid": mid_instruments, 105 | "hi": hi_instruments, 106 | } 107 | 108 | if band not in range_map: 109 | raise ValueError(f"Invalid band `{band}`. Must be low, mid, or hi") 110 | 111 | return (roll[:, range_map[band]].sum(axis=1) > 0).astype(int) 112 | 113 | 114 | def get_bands(roll): 115 | """Parses the low, mid, and high frequency bands of a piano roll""" 116 | return ( 117 | get_band(roll, band="low"), 118 | get_band(roll, band="mid"), 119 | get_band(roll, band="hi"), 120 | ) 121 | 122 | 123 | def event_to_8number(midi_notes): 124 | # input an event list and output a representation 125 | # in 8 instrumental band: 126 | # kick, snare, rimshot, clap, closed hihat, open hihat, low tom, high tom 127 | output = [] 128 | # make sure the event has notes 129 | if len(midi_notes) == 0: 130 | return [0] 131 | 132 | for x in midi_notes: 133 | # print("x", x) 134 | output.append(GM_dict[x][4]) 135 | 136 | # otherwise it is a silence 137 | output = list(set(output)) 138 | output.sort() 139 | 140 | return output 141 | 142 | 143 | def event_to_3number(midi_notes): 144 | # input an event list and output a representation 145 | # in 3 instrumental band: 146 | # low, mid, high 147 | output = [] 148 | # make sure the event has notes 149 | if len(midi_notes) == 0: 150 | return [0] 151 | 152 | for x in midi_notes: 153 | category = GM_dict[x][1] 154 | if category == "low": 155 | category_number = 1 156 | elif category == "mid": 157 | category_number = 2 158 | else: 159 | category_number = 3 160 | output.append(category_number) 161 | 162 | # otherwise it is a silence 163 | output = list(set(output)) 164 | output.sort() 165 | 166 | return output 167 | -------------------------------------------------------------------------------- /tests/test_descriptors.py: -------------------------------------------------------------------------------- 1 | from .fixtures import ( 2 | BOSKA_3, 3 | BOSKA_3_DESCRIPTORS, 4 | BOSKA_8, 5 | BOSKA_8_DESCRIPTORS, 6 | BOSKA_9, 7 | BOSKA_9_DESCRIPTORS, 8 | PATT_1, 9 | PATT_2, 10 | ) 11 | 12 | from rhythmtoolbox.descriptors import ( 13 | balance, 14 | bandness, 15 | density, 16 | evenness, 17 | get_n_onset_steps, 18 | noi, 19 | poly_balance, 20 | poly_density, 21 | poly_evenness, 22 | poly_sync, 23 | step_density, 24 | syncopation16, 25 | syncopation16_awareness, 26 | syness, 27 | ) 28 | from rhythmtoolbox.midi_mapping import get_bands 29 | 30 | 31 | def test_syncopation16_awareness(): 32 | assert syncopation16_awareness(PATT_1) == -11 33 | assert syncopation16_awareness(PATT_2) == -39 34 | 35 | 36 | def test_evenness(): 37 | assert evenness(PATT_1) == 0.9816064222042191 38 | assert evenness(PATT_2) == 0.971165288619607 39 | 40 | 41 | def test_balance(): 42 | assert balance(PATT_1) == 0.9297693395285831 43 | assert balance(PATT_2) == 0.9609819355967744 44 | 45 | 46 | def test_noi(): 47 | assert noi(BOSKA_3) == BOSKA_3_DESCRIPTORS["noi"] 48 | assert noi(BOSKA_8) == BOSKA_8_DESCRIPTORS["noi"] 49 | assert noi(BOSKA_9) == BOSKA_9_DESCRIPTORS["noi"] 50 | 51 | 52 | def test_density(): 53 | lowband, midband, hiband = get_bands(BOSKA_3) 54 | assert density(lowband) == BOSKA_3_DESCRIPTORS["lowDensity"] 55 | assert density(midband) == BOSKA_3_DESCRIPTORS["midDensity"] 56 | assert density(hiband) == BOSKA_3_DESCRIPTORS["hiDensity"] 57 | 58 | lowband, midband, hiband = get_bands(BOSKA_8) 59 | assert density(lowband) == BOSKA_8_DESCRIPTORS["lowDensity"] 60 | assert density(midband) == BOSKA_8_DESCRIPTORS["midDensity"] 61 | assert density(hiband) == BOSKA_8_DESCRIPTORS["hiDensity"] 62 | 63 | lowband, midband, hiband = get_bands(BOSKA_9) 64 | assert density(lowband) == BOSKA_9_DESCRIPTORS["lowDensity"] 65 | assert density(midband) == BOSKA_9_DESCRIPTORS["midDensity"] 66 | assert density(hiband) == BOSKA_9_DESCRIPTORS["hiDensity"] 67 | 68 | 69 | def test_stepDensity(): 70 | assert step_density(BOSKA_3) == BOSKA_3_DESCRIPTORS["stepDensity"] 71 | assert step_density(BOSKA_8) == BOSKA_8_DESCRIPTORS["stepDensity"] 72 | assert step_density(BOSKA_9) == BOSKA_9_DESCRIPTORS["stepDensity"] 73 | 74 | 75 | def test_bandness(): 76 | n_onset_steps = get_n_onset_steps(BOSKA_3) 77 | lowband, midband, hiband = get_bands(BOSKA_3) 78 | assert bandness(lowband, n_onset_steps) == BOSKA_3_DESCRIPTORS["lowness"] 79 | assert bandness(midband, n_onset_steps) == BOSKA_3_DESCRIPTORS["midness"] 80 | assert bandness(hiband, n_onset_steps) == BOSKA_3_DESCRIPTORS["hiness"] 81 | 82 | n_onset_steps = get_n_onset_steps(BOSKA_8) 83 | lowband, midband, hiband = get_bands(BOSKA_8) 84 | assert bandness(lowband, n_onset_steps) == BOSKA_8_DESCRIPTORS["lowness"] 85 | assert bandness(midband, n_onset_steps) == BOSKA_8_DESCRIPTORS["midness"] 86 | assert bandness(hiband, n_onset_steps) == BOSKA_8_DESCRIPTORS["hiness"] 87 | 88 | n_onset_steps = get_n_onset_steps(BOSKA_9) 89 | lowband, midband, hiband = get_bands(BOSKA_9) 90 | assert bandness(lowband, n_onset_steps) == BOSKA_9_DESCRIPTORS["lowness"] 91 | assert bandness(midband, n_onset_steps) == BOSKA_9_DESCRIPTORS["midness"] 92 | assert bandness(hiband, n_onset_steps) == BOSKA_9_DESCRIPTORS["hiness"] 93 | 94 | 95 | def test_syncopation16(): 96 | assert syncopation16(PATT_1) == -4 97 | assert syncopation16(PATT_2) == -10 98 | 99 | lowband, midband, hiband = get_bands(BOSKA_3) 100 | assert syncopation16(lowband) == BOSKA_3_DESCRIPTORS["lowSync"] 101 | assert syncopation16(midband) == BOSKA_3_DESCRIPTORS["midSync"] 102 | assert syncopation16(hiband) == BOSKA_3_DESCRIPTORS["hiSync"] 103 | lowband, midband, hiband = get_bands(BOSKA_8) 104 | assert syncopation16(lowband) == BOSKA_8_DESCRIPTORS["lowSync"] 105 | assert syncopation16(midband) == BOSKA_8_DESCRIPTORS["midSync"] 106 | assert syncopation16(hiband) == BOSKA_8_DESCRIPTORS["hiSync"] 107 | lowband, midband, hiband = get_bands(BOSKA_9) 108 | assert syncopation16(lowband) == BOSKA_9_DESCRIPTORS["lowSync"] 109 | assert syncopation16(midband) == BOSKA_9_DESCRIPTORS["midSync"] 110 | assert syncopation16(hiband) == BOSKA_9_DESCRIPTORS["hiSync"] 111 | 112 | 113 | def test_syness(): 114 | lowband, midband, hiband = get_bands(BOSKA_3) 115 | assert syness(lowband) == BOSKA_3_DESCRIPTORS["lowSyness"] 116 | assert syness(midband) == BOSKA_3_DESCRIPTORS["midSyness"] 117 | assert syness(hiband) == BOSKA_3_DESCRIPTORS["hiSyness"] 118 | 119 | lowband, midband, hiband = get_bands(BOSKA_8) 120 | assert syness(lowband) == BOSKA_8_DESCRIPTORS["lowSyness"] 121 | assert syness(midband) == BOSKA_8_DESCRIPTORS["midSyness"] 122 | assert syness(hiband) == BOSKA_8_DESCRIPTORS["hiSyness"] 123 | 124 | lowband, midband, hiband = get_bands(BOSKA_9) 125 | assert syness(lowband) == BOSKA_9_DESCRIPTORS["lowSyness"] 126 | assert syness(midband) == BOSKA_9_DESCRIPTORS["midSyness"] 127 | assert syness(hiband) == BOSKA_9_DESCRIPTORS["hiSyness"] 128 | 129 | 130 | def test_polySync(): 131 | lowband, midband, hiband = get_bands(BOSKA_3) 132 | assert poly_sync(lowband, midband, hiband) == BOSKA_3_DESCRIPTORS["polySync"] 133 | 134 | lowband, midband, hiband = get_bands(BOSKA_8) 135 | assert poly_sync(lowband, midband, hiband) == BOSKA_8_DESCRIPTORS["polySync"] 136 | 137 | lowband, midband, hiband = get_bands(BOSKA_9) 138 | assert poly_sync(lowband, midband, hiband) == BOSKA_9_DESCRIPTORS["polySync"] 139 | 140 | 141 | def test_polyEvenness(): 142 | lowband, midband, hiband = get_bands(BOSKA_3) 143 | assert ( 144 | poly_evenness(lowband, midband, hiband) == BOSKA_3_DESCRIPTORS["polyEvenness"] 145 | ) 146 | 147 | lowband, midband, hiband = get_bands(BOSKA_8) 148 | assert ( 149 | poly_evenness(lowband, midband, hiband) == BOSKA_8_DESCRIPTORS["polyEvenness"] 150 | ) 151 | 152 | lowband, midband, hiband = get_bands(BOSKA_9) 153 | assert ( 154 | poly_evenness(lowband, midband, hiband) == BOSKA_9_DESCRIPTORS["polyEvenness"] 155 | ) 156 | 157 | 158 | def test_polyBalance(): 159 | lowband, midband, hiband = get_bands(BOSKA_3) 160 | assert poly_balance(lowband, midband, hiband) == BOSKA_3_DESCRIPTORS["polyBalance"] 161 | 162 | lowband, midband, hiband = get_bands(BOSKA_8) 163 | assert poly_balance(lowband, midband, hiband) == BOSKA_8_DESCRIPTORS["polyBalance"] 164 | 165 | lowband, midband, hiband = get_bands(BOSKA_9) 166 | assert poly_balance(lowband, midband, hiband) == BOSKA_9_DESCRIPTORS["polyBalance"] 167 | 168 | 169 | def test_polyDensity(): 170 | lowband, midband, hiband = get_bands(BOSKA_3) 171 | assert poly_density(lowband, midband, hiband) == BOSKA_3_DESCRIPTORS["polyDensity"] 172 | 173 | lowband, midband, hiband = get_bands(BOSKA_8) 174 | assert poly_density(lowband, midband, hiband) == BOSKA_8_DESCRIPTORS["polyDensity"] 175 | 176 | lowband, midband, hiband = get_bands(BOSKA_9) 177 | assert poly_density(lowband, midband, hiband) == BOSKA_9_DESCRIPTORS["polyDensity"] 178 | -------------------------------------------------------------------------------- /tests/test_init.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from .fixtures import ( 3 | BOSKA_3, 4 | BOSKA_3_DESCRIPTORS, 5 | BOSKA_3_PATTLIST, 6 | BOSKA_8, 7 | BOSKA_8_DESCRIPTORS, 8 | BOSKA_8_PATTLIST, 9 | BOSKA_9, 10 | BOSKA_9_DESCRIPTORS, 11 | BOSKA_9_PATTLIST, 12 | FOUR_KICKS_DESCRIPTORS, 13 | UNBALANCED_1_DESCRIPTORS, 14 | ) 15 | 16 | from rhythmtoolbox import ( 17 | midifile2descriptors, 18 | pattlist2descriptors, 19 | pianoroll2descriptors, 20 | ) 21 | 22 | 23 | def test_midifile2descriptors(): 24 | file_descriptors = { 25 | "midi/boska/3.mid": BOSKA_3_DESCRIPTORS, 26 | "midi/two_bar/four_kicks.mid": FOUR_KICKS_DESCRIPTORS, 27 | "midi/two_bar/unbalanced_1.mid": UNBALANCED_1_DESCRIPTORS, 28 | } 29 | for f in file_descriptors: 30 | v = midifile2descriptors(f) 31 | assert set(v) == set(file_descriptors[f]) 32 | for k in v: 33 | assert np.isclose(v[k], file_descriptors[f][k]) 34 | 35 | v = midifile2descriptors("midi/boska/8.mid") 36 | assert np.isclose(v["noi"], 5) 37 | assert np.isclose(v["polyDensity"], 12) 38 | assert np.isclose(v["lowDensity"], 2) 39 | assert np.isclose(v["midDensity"], 7) 40 | assert np.isclose(v["hiDensity"], 3) 41 | assert np.isclose(v["lowness"], 0.2222222222222222) 42 | assert np.isclose(v["midness"], 0.7777777777777778) 43 | assert np.isclose(v["hiness"], 0.3333333333333333) 44 | assert np.isclose(v["stepDensity"], 0.5625) 45 | assert np.isclose(v["sync"], -4) 46 | assert np.isclose(v["lowSync"], 3) 47 | assert np.isclose(v["midSync"], -11) 48 | assert np.isclose(v["hiSync"], -7) 49 | assert np.isclose(v["syness"], -0.4444444444444444) 50 | assert np.isclose(v["lowSyness"], 1.5) 51 | assert np.isclose(v["midSyness"], -1.5714285714285714) 52 | assert np.isclose(v["hiSyness"], -2.3333333333333335) 53 | assert np.isclose(v["balance"], 0.8779951001768869) 54 | assert np.isclose(v["polyBalance"], 0.9023419344890125) 55 | assert np.isclose(v["evenness"], 0.9462279747433523) 56 | assert np.isclose(v["polyEvenness"], 5.099656633584969) 57 | assert np.isclose(v["polySync"], 0) 58 | 59 | v = midifile2descriptors("midi/boska/9.mid") 60 | assert np.isclose(v["noi"], 6) 61 | assert np.isclose(v["polyDensity"], 17) 62 | assert np.isclose(v["lowDensity"], 4) 63 | assert np.isclose(v["midDensity"], 6) 64 | assert np.isclose(v["hiDensity"], 7) 65 | assert np.isclose(v["lowness"], 0.4444444444444444) 66 | assert np.isclose(v["midness"], 0.6666666666666666) 67 | assert np.isclose(v["hiness"], 0.7777777777777778) 68 | assert np.isclose(v["stepDensity"], 0.5625) 69 | assert np.isclose(v["sync"], -10) 70 | assert np.isclose(v["lowSync"], -6) 71 | assert np.isclose(v["midSync"], -3) 72 | assert np.isclose(v["hiSync"], -14) 73 | assert np.isclose(v["syness"], -1.1111111111111112) 74 | assert np.isclose(v["lowSyness"], -1.5) 75 | assert np.isclose(v["midSyness"], -0.5) 76 | assert np.isclose(v["hiSyness"], -2.0) 77 | assert np.isclose(v["balance"], 0.905804548330825) 78 | assert np.isclose(v["polyBalance"], 0.7646388055112554) 79 | assert np.isclose(v["evenness"], 0.9842351591376168) 80 | assert np.isclose(v["polyEvenness"], 4.930845709389592) 81 | assert np.isclose(v["polySync"], 3) 82 | 83 | 84 | def test_pianoroll2descriptors(): 85 | v = pianoroll2descriptors(BOSKA_3) 86 | assert set(v) == set(BOSKA_3_DESCRIPTORS) 87 | for k in v: 88 | assert np.isclose(v[k], BOSKA_3_DESCRIPTORS[k]) 89 | 90 | v = pianoroll2descriptors(BOSKA_3, drums=False) 91 | assert set(v) == set(BOSKA_3_DESCRIPTORS) 92 | assert np.isclose(v["noi"], 5) 93 | assert np.isclose(v["polyDensity"], 11) 94 | assert v["lowDensity"] is None 95 | assert v["midDensity"] is None 96 | assert v["hiDensity"] is None 97 | assert v["lowness"] is None 98 | assert v["midness"] is None 99 | assert v["hiness"] is None 100 | assert np.isclose(v["stepDensity"], 0.6875) 101 | assert np.isclose(v["sync"], -10) 102 | assert v["lowSync"] is None 103 | assert v["midSync"] is None 104 | assert v["hiSync"] is None 105 | assert np.isclose(v["syness"], -0.9090909090909091) 106 | assert v["lowSyness"] is None 107 | assert v["midSyness"] is None 108 | assert v["hiSyness"] is None 109 | assert np.isclose(v["balance"], 0.9623442216024459) 110 | assert v["polyBalance"] is None 111 | assert np.isclose(v["evenness"], 0.9841710008484362) 112 | assert v["polyEvenness"] is None 113 | assert v["polySync"] is None 114 | 115 | v = pianoroll2descriptors(BOSKA_8) 116 | assert set(v) == set(BOSKA_8_DESCRIPTORS) 117 | for k in v: 118 | assert np.isclose(v[k], BOSKA_8_DESCRIPTORS[k]) 119 | 120 | v = pianoroll2descriptors(BOSKA_8, drums=False) 121 | assert set(v) == set(BOSKA_8_DESCRIPTORS) 122 | assert np.isclose(v["noi"], 5) 123 | assert np.isclose(v["polyDensity"], 10) 124 | assert v["lowDensity"] is None 125 | assert v["midDensity"] is None 126 | assert v["hiDensity"] is None 127 | assert v["lowness"] is None 128 | assert v["midness"] is None 129 | assert v["hiness"] is None 130 | assert np.isclose(v["stepDensity"], 0.625) 131 | assert np.isclose(v["sync"], -2) 132 | assert v["lowSync"] is None 133 | assert v["midSync"] is None 134 | assert v["hiSync"] is None 135 | assert np.isclose(v["syness"], -0.2) 136 | assert v["lowSyness"] is None 137 | assert v["midSyness"] is None 138 | assert v["hiSyness"] is None 139 | assert np.isclose(v["balance"], 0.8186690031367755) 140 | assert v["polyBalance"] is None 141 | assert np.isclose(v["evenness"], 0.8820076387881416) 142 | assert v["polyEvenness"] is None 143 | assert v["polySync"] is None 144 | 145 | v = pianoroll2descriptors(BOSKA_9) 146 | assert set(v) == set(BOSKA_9_DESCRIPTORS) 147 | for k in v: 148 | assert np.isclose(v[k], BOSKA_9_DESCRIPTORS[k]) 149 | 150 | v = pianoroll2descriptors(BOSKA_9, drums=False) 151 | assert set(v) == set(BOSKA_9_DESCRIPTORS) 152 | assert np.isclose(v["noi"], 6) 153 | assert np.isclose(v["polyDensity"], 9) 154 | assert v["lowDensity"] is None 155 | assert v["midDensity"] is None 156 | assert v["hiDensity"] is None 157 | assert v["lowness"] is None 158 | assert v["midness"] is None 159 | assert v["hiness"] is None 160 | assert np.isclose(v["stepDensity"], 0.5625) 161 | assert np.isclose(v["sync"], -10) 162 | assert v["lowSync"] is None 163 | assert v["midSync"] is None 164 | assert v["hiSync"] is None 165 | assert np.isclose(v["syness"], -1.1111111111111112) 166 | assert v["lowSyness"] is None 167 | assert v["midSyness"] is None 168 | assert v["hiSyness"] is None 169 | assert np.isclose(v["balance"], 0.905804548330825) 170 | assert v["polyBalance"] is None 171 | assert np.isclose(v["evenness"], 0.9842351591376168) 172 | assert v["polyEvenness"] is None 173 | assert v["polySync"] is None 174 | 175 | 176 | def test_pattlist2descriptors(): 177 | v = pattlist2descriptors(BOSKA_3_PATTLIST) 178 | assert set(v) == set(BOSKA_3_DESCRIPTORS) 179 | for k in v: 180 | assert np.isclose(v[k], BOSKA_3_DESCRIPTORS[k]) 181 | 182 | v = pattlist2descriptors(BOSKA_8_PATTLIST) 183 | assert set(v) == set(BOSKA_8_DESCRIPTORS) 184 | for k in v: 185 | assert np.isclose(v[k], BOSKA_8_DESCRIPTORS[k]) 186 | 187 | v = pattlist2descriptors(BOSKA_9_PATTLIST) 188 | assert set(v) == set(BOSKA_9_DESCRIPTORS) 189 | for k in v: 190 | assert np.isclose(v[k], BOSKA_9_DESCRIPTORS[k]) 191 | -------------------------------------------------------------------------------- /rhythmtoolbox/__init__.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | import numpy as np 4 | import pretty_midi as pm 5 | from scipy import ndimage 6 | 7 | from rhythmtoolbox.descriptors import ( 8 | balance, 9 | bandness, 10 | density, 11 | evenness, 12 | get_n_onset_steps, 13 | noi, 14 | poly_balance, 15 | poly_density, 16 | poly_evenness, 17 | poly_sync, 18 | step_density, 19 | syncopation16, 20 | syness, 21 | ) 22 | from rhythmtoolbox.midi_mapping import get_bands 23 | 24 | DESCRIPTOR_NAMES = [ 25 | "noi", 26 | "polyDensity", 27 | "lowDensity", 28 | "midDensity", 29 | "hiDensity", 30 | "lowness", 31 | "midness", 32 | "hiness", 33 | "stepDensity", 34 | "sync", 35 | "lowSync", 36 | "midSync", 37 | "hiSync", 38 | "syness", 39 | "lowSyness", 40 | "midSyness", 41 | "hiSyness", 42 | "balance", 43 | "polyBalance", 44 | "evenness", 45 | "polyEvenness", 46 | "polySync", 47 | ] 48 | 49 | 50 | def pattlist_to_pianoroll(pattlist): 51 | """Convert a pattern list to a piano roll""" 52 | roll = np.zeros((len(pattlist), 128)) 53 | for i in range(len(roll)): 54 | roll[i, pattlist[i]] = 1 55 | return roll 56 | 57 | 58 | def resample_pianoroll(roll, from_resolution, to_resolution): 59 | """Associate each onset in the roll with its closest 16th note position""" 60 | 61 | if from_resolution == to_resolution: 62 | return roll 63 | 64 | assert len(roll.shape) == 2, "Piano roll must be a 2D array" 65 | 66 | factor = to_resolution / from_resolution 67 | 68 | return ndimage.zoom(roll, (factor, 1), order=0) 69 | 70 | 71 | def pianoroll2descriptors(roll, resolution=4, drums=True): 72 | """Compute all descriptors from a piano roll representation of a polyphonic drum pattern. 73 | 74 | Notes 75 | - A piano roll with a resolution other than 4 ticks per beat will be resampled, which can be lossy. 76 | - Some descriptors are valid only for 16-step patterns and will be None if the pattern is not divisible by 16. 77 | 78 | Parameters 79 | roll, np.ndarray 80 | The piano roll 81 | 82 | resolution, int 83 | The resolution of the piano roll in MIDI ticks per beat 84 | 85 | drums, bool 86 | Indicates whether the pattern is a drum pattern 87 | 88 | Returns 89 | Descriptors in a dict of {descriptor_name: descriptor_value} 90 | """ 91 | 92 | assert len(roll.shape) == 2, "Piano roll must be a 2D array" 93 | 94 | # Initialize the output dictionary 95 | result = {d: None for d in DESCRIPTOR_NAMES} 96 | 97 | # Resample to a 16-note resolution 98 | resampled = resample_pianoroll(roll, resolution, 4) 99 | 100 | # No need to compute descriptors for empty patterns 101 | n_onset_steps = get_n_onset_steps(resampled) 102 | if n_onset_steps == 0: 103 | return result 104 | 105 | pattern = (resampled.sum(axis=1) > 0).astype(int) 106 | 107 | if not drums: 108 | result["noi"] = noi(resampled) 109 | result["stepDensity"] = step_density(resampled) 110 | result["polyDensity"] = density(pattern) 111 | 112 | if len(resampled) % 16 == 0: 113 | sub_descs = defaultdict(list) 114 | for subroll in np.split(resampled, len(resampled) / 16): 115 | subpattern = (subroll.sum(axis=1) > 0).astype(int) 116 | sub_descs["balance"] = balance(subpattern) 117 | sub_descs["evenness"] = evenness(subpattern) 118 | sub_descs["sync"] = syncopation16(subpattern) 119 | sub_descs["syness"] = syness(subpattern) 120 | 121 | for desc in sub_descs: 122 | result[desc] = np.mean(sub_descs[desc]) 123 | 124 | for desc in DESCRIPTOR_NAMES: 125 | if desc not in result: 126 | result[desc] = None 127 | 128 | return result 129 | 130 | # Get the onset pattern of each frequency band 131 | low_band, mid_band, hi_band = get_bands(resampled) 132 | 133 | # Compute descriptors that are valid for any pattern length 134 | result["noi"] = noi(resampled) 135 | result["lowDensity"] = density(low_band) 136 | result["midDensity"] = density(mid_band) 137 | result["hiDensity"] = density(hi_band) 138 | result["polyDensity"] = poly_density(low_band, mid_band, hi_band) 139 | result["stepDensity"] = step_density(resampled) 140 | result["lowness"] = bandness(low_band, n_onset_steps) 141 | result["midness"] = bandness(mid_band, n_onset_steps) 142 | result["hiness"] = bandness(hi_band, n_onset_steps) 143 | 144 | # Compute descriptors that are valid only for 16-step patterns 145 | # For patterns w lengths greater than and divisible by 16, compute descriptors for each 16-step subpattern and return the mean 146 | if len(resampled) % 16 == 0: 147 | sub_descs = defaultdict(list) 148 | for subroll in np.split(resampled, len(resampled) / 16): 149 | sub_low, sub_mid, sub_hi = get_bands(subroll) 150 | subpattern = (subroll.sum(axis=1) > 0).astype(int) 151 | 152 | sub_descs["sync"].append(syncopation16(subpattern)) 153 | sub_descs["lowSync"].append(syncopation16(sub_low)) 154 | sub_descs["midSync"].append(syncopation16(sub_mid)) 155 | sub_descs["hiSync"].append(syncopation16(sub_hi)) 156 | sub_descs["syness"].append(syness(subpattern)) 157 | sub_descs["lowSyness"].append(syness(sub_low)) 158 | sub_descs["midSyness"].append(syness(sub_mid)) 159 | sub_descs["hiSyness"].append(syness(sub_hi)) 160 | sub_descs["balance"].append(balance(subpattern)) 161 | sub_descs["polyBalance"].append(poly_balance(sub_low, sub_mid, sub_hi)) 162 | sub_descs["evenness"].append(evenness(subpattern)) 163 | sub_descs["polyEvenness"].append(poly_evenness(sub_low, sub_mid, sub_hi)) 164 | sub_descs["polySync"].append(poly_sync(sub_low, sub_mid, sub_hi)) 165 | 166 | for desc in sub_descs: 167 | result[desc] = np.mean(sub_descs[desc]) 168 | 169 | return result 170 | 171 | 172 | def pattlist2descriptors(pattlist, resolution=4, drums=True): 173 | """Compute all descriptors from a pattern list representation of a polyphonic drum pattern. 174 | 175 | A pattern list is a list of lists representing time steps, each containing the MIDI note numbers that occur at that 176 | step, e.g. [[36, 42], [], [37], []]. Velocity is not included. 177 | 178 | Parameters 179 | pattlist, list 180 | The pattern list 181 | 182 | resolution, int 183 | The resolution of the piano roll in MIDI ticks per beat 184 | 185 | drums, bool 186 | Indicates whether the pattern is a drum pattern 187 | 188 | Returns 189 | Descriptors in a dict of {descriptor_name: descriptor_value} 190 | """ 191 | roll = pattlist_to_pianoroll(pattlist) 192 | return pianoroll2descriptors(roll, resolution, drums=drums) 193 | 194 | 195 | def get_subdivisions(pmid, resolution): 196 | """Parse beats from a PrettyMIDI object and create an array of subdivisions at a given resolution. 197 | 198 | :param pmid: PrettyMIDI object 199 | :param resolution: Resolution of the output array 200 | :return: Array of subdivisions 201 | """ 202 | beats = pmid.get_beats() 203 | 204 | # Assume a single 4-beat bar 205 | if len(beats) <= 1: 206 | beats = np.arange(0, 4) 207 | 208 | beat_sep = beats[-1] - beats[-2] 209 | additional_beat = beats[-1] + beat_sep 210 | additional_beats = np.array(additional_beat) 211 | if additional_beat % 1 != 0: 212 | additional_beats = np.append(additional_beats, additional_beat + beat_sep) 213 | beats = np.append(beats, additional_beats) 214 | 215 | # Upsample beat times to the input resolution using linear interpolation 216 | subdivisions = [] 217 | for start, end in zip(beats, beats[1:]): 218 | for j in range(resolution): 219 | subdivisions.append((end - start) / resolution * j + start) 220 | subdivisions.append(beats[-1]) 221 | 222 | return np.array(subdivisions) 223 | 224 | 225 | def get_onset_roll_from_pmid(pmid, resolution=4): 226 | """Converts a PrettyMIDI object to a piano roll at the given resolution, preserving only onsets. 227 | 228 | - If input MIDI is multi-track, we consider only the first track 229 | - The input MIDI is quantized to the given resolution 230 | 231 | :param pmid: PrettyMIDI object 232 | :param resolution: Resolution of the piano roll in MIDI ticks per beat 233 | :return: Onset roll of shape (N, V), where N is the number of time steps and V is the number of MIDI pitches 234 | """ 235 | if not pmid.instruments: 236 | return np.zeros((0, 128), np.uint8) 237 | 238 | # Consider only the first instrument 239 | instrument = pmid.instruments[0] 240 | if len(instrument.notes) == 0: 241 | return np.zeros((0, 128), np.uint8) 242 | 243 | subdivisions = get_subdivisions(pmid, resolution=resolution) 244 | 245 | n_ticks = len(subdivisions) - 1 246 | 247 | onsets_unquantized = [note.start for note in instrument.notes] 248 | onsets = [np.argmin(np.abs(t - subdivisions)) for t in onsets_unquantized] 249 | 250 | # If an onset is quantized to the last tick, move it to the previous tick 251 | for ix, onset in enumerate(onsets): 252 | if onset == n_ticks: 253 | onsets[onsets.index(onset)] = onset - 1 254 | 255 | pitches = [note.pitch for note in instrument.notes] 256 | velocities = [note.velocity for note in instrument.notes] 257 | 258 | onset_roll = np.zeros((n_ticks, 128), np.uint8) 259 | onset_roll[onsets, pitches] = velocities 260 | 261 | return onset_roll 262 | 263 | 264 | def midifile2descriptors(filepath, drums=True): 265 | """Compute all descriptors from a MIDI file. 266 | 267 | Parameters 268 | filepath, str 269 | Path to a MIDI file 270 | 271 | drums, bool 272 | Indicates whether the pattern is a drum pattern 273 | 274 | Returns 275 | Descriptors in a dict of {descriptor_name: descriptor_value} 276 | """ 277 | pmid = pm.PrettyMIDI(filepath, resolution=4) 278 | onset_roll = get_onset_roll_from_pmid(pmid) 279 | return pianoroll2descriptors(onset_roll, drums=drums) 280 | -------------------------------------------------------------------------------- /rhythmtoolbox/descriptors.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements various descriptors derived from scientific publications related to polyphonic drum pattern 3 | analysis and generation. 4 | 5 | References 6 | 7 | - Gómez Marín, D. (2018). Similarity and style in electronic dance music drum rhythms (Doctoral dissertation, 8 | Universitat Pompeu Fabra). 9 | 10 | - Gómez-Marín, D., Jorda, S., & Herrera, P. (2016). Strictly Rhythm: Exploring the effects of identical regions and 11 | meter induction in rhythmic similarity perception. In Music, Mind, and Embodiment: 11th International Symposium, CMMR 12 | 2015, Plymouth, UK, June 16-19, 2015, Revised Selected Papers 11 (pp. 449-463). Springer International Publishing. 13 | 14 | - Gómez Marín, D., Jordà Puig, S., & Boyer, H. (2015). Pad and Sad: Two awareness-Weighted rhythmic similarity 15 | distances. In Müller M, Wiering F, editors. Proceedings of the 16th International Society for Music Information 16 | Retrieval (ISMIR) Conference; 2015 Oct 26-30; Málaga, Spain. Canada: International Society for Music Information 17 | Retrieval; 2015.. International Society for Music Information Retrieval (ISMIR). 18 | 19 | - Gómez-Marín, D., Jordà, S., & Herrera, P. (2020). Drum rhythm spaces: From polyphonic similarity to generative maps. 20 | Journal of New Music Research, 49(5), 438-456. 21 | 22 | - Haki, B., Nieto, M., Pelinski, T., & Jordà, S. Real-Time Drum Accompaniment Using Transformer Architecture. 23 | 24 | - Milne, A. J., & Dean, R. T. (2016). Computational creation and morphing of multilevel rhythms by control of evenness 25 | Computer Music Journal, 40(1), 35-53. 26 | 27 | - Milne, A. J., & Herff, S. A. (2020). The perceptual relevance of balance, evenness, and entropy in musical rhythms. 28 | Cognition, 203, 104233. 29 | 30 | - Witek, M. A., Clarke, E. F., Wallentin, M., Kringelbach, M. L., & Vuust, P. (2014). Syncopation, body-movement and 31 | pleasure in groove music. PloS one, 9(4), e94446. 32 | """ 33 | 34 | import math 35 | 36 | import numpy as np 37 | 38 | # Monophonic descriptors 39 | 40 | 41 | def syncopation16(pattern): 42 | """Compute the syncopation value of a 16-step pattern 43 | 44 | pattern, list 45 | a monophonic pattern as a list of 0s and 1s (1s indicating an onset) 46 | """ 47 | 48 | if isinstance(pattern, np.ndarray): 49 | pattern = pattern.tolist() 50 | 51 | synclist = [0] * 16 52 | salience_lhl = [5, 1, 2, 1, 3, 1, 2, 1, 4, 1, 2, 1, 3, 1, 2, 1] 53 | 54 | n_steps = len(pattern) 55 | for ix in range(n_steps): 56 | next_ix = (ix + 1) % n_steps 57 | # look for an onset preceding a silence 58 | if pattern[ix] == 1 and pattern[next_ix] == 0: 59 | # compute syncopation 60 | synclist[ix] = salience_lhl[next_ix] - salience_lhl[ix] 61 | 62 | return sum(synclist) 63 | 64 | 65 | def syncopation16_awareness(pattern): 66 | # input a monophonic pattern as a list of 0s and 1s (1s indicating an onset) 67 | # and obtain its awareness-weighted syncopation value 68 | # awareness is reported in [2] 69 | synclist = [0] * 16 70 | salience = [5, 1, 2, 1, 3, 1, 2, 1, 4, 1, 2, 1, 3, 1, 2, 1] 71 | awareness = [5, 1, 4, 2] 72 | n_steps = len(pattern) 73 | for ix in range(n_steps): 74 | next_ix = (ix + 1) % n_steps 75 | # look for an onset and a silence following 76 | if pattern[ix] == 1 and pattern[next_ix] == 0: 77 | # compute syncopation 78 | synclist[ix] = salience[next_ix] - salience[ix] 79 | 80 | # apply awareness 81 | sync_and_awareness = [ 82 | sum(synclist[0:4]) * awareness[0], 83 | sum(synclist[4:8]) * awareness[1], 84 | sum(synclist[8:12]) * awareness[2], 85 | sum(synclist[12:16]) * awareness[3], 86 | ] 87 | 88 | return sum(sync_and_awareness) 89 | 90 | 91 | def evenness(pattern): 92 | # how well distributed are the D onsets of a pattern 93 | # if they are compared to a perfect D sided polygon 94 | # input patterns are phase-corrected to start always at step 0 95 | # i.e. if we have 4 onsets in a 16 step pattern, what is the distance of onsets 96 | # o1, o2, o3, o4 to positions 0 4 8 and 12 97 | # here we will use a simple algorithm that does not involve DFT computation 98 | # evenness is well described in [Milne and Dean, 2016] but this implementation is much simpler 99 | d = density(pattern) 100 | if d == 0: 101 | return 0 102 | 103 | iso_angle_16 = 2 * math.pi / 16 104 | first_onset_step = [i for i, x in enumerate(pattern) if x == 1][0] 105 | first_onset_angle = first_onset_step * iso_angle_16 106 | iso_angle = 2 * math.pi / d 107 | iso_pattern_radians = [x * iso_angle for x in range(d)] 108 | pattern_radians = [i * iso_angle_16 for i, x in enumerate(pattern) if x == 1] 109 | cosines = [ 110 | abs(math.cos(x - pattern_radians[i] + first_onset_angle)) 111 | for i, x in enumerate(iso_pattern_radians) 112 | ] 113 | return sum(cosines) / d 114 | 115 | 116 | def balance(pattern): 117 | # balance is described in [Milne and Herff, 2020] as: 118 | # "a quantification of the proximity of that rhythm's 119 | # “centre of mass” (the mean position of the points) 120 | # to the centre of the unit circle." 121 | d = density(pattern) 122 | if d == 0: 123 | return 1 124 | 125 | center = np.array([0, 0]) 126 | iso_angle_16 = 2 * math.pi / 16 127 | X = [math.cos(i * iso_angle_16) for i, x in enumerate(pattern) if x == 1] 128 | Y = [math.sin(i * iso_angle_16) for i, x in enumerate(pattern) if x == 1] 129 | matrix = np.array([X, Y]) 130 | matrix_sum = matrix.sum(axis=1) 131 | magnitude = np.linalg.norm(matrix_sum - center) / d 132 | return 1 - magnitude 133 | 134 | 135 | def density(pattern): 136 | """Computes the density of the pattern.""" 137 | return sum(pattern) 138 | 139 | 140 | # Polyphonic descriptors 141 | 142 | 143 | def noi(roll): 144 | """Returns the number of instruments (noi) used in the roll""" 145 | return (roll.sum(axis=0) > 0).sum() 146 | 147 | 148 | def get_n_onset_steps(roll): 149 | """Returns the number of steps with onsets""" 150 | return (roll.sum(axis=1) > 0).sum() 151 | 152 | 153 | def step_density(roll): 154 | """Returns the percentage of steps with onsets""" 155 | return get_n_onset_steps(roll) / len(roll) 156 | 157 | 158 | def bandness(pattern, n_onset_steps): 159 | """Computes a measure of how concentrated the pattern is in the given frequency band. 160 | 161 | pattern, list 162 | The pattern of the band of interest. 163 | 164 | n_onset_steps, int 165 | The number of steps with onsets of the entire roll. 166 | """ 167 | return density(pattern) / n_onset_steps if n_onset_steps else 0 168 | 169 | 170 | def syness(pattern): 171 | """Returns the syncopation of the pattern divided by the number of onsets in the pattern""" 172 | d = density(pattern) 173 | return syncopation16(pattern) / d if d else 0 174 | 175 | 176 | def poly_sync(low_stream, mid_stream, hi_stream): 177 | """Computes the polyphonic syncopation of a rhythm, as described in [Witek et al., 2014]. 178 | 179 | If N is a note that precedes a rest R, and R has a metric weight greater than or equal to N, then the pair (N, R) 180 | is said to constitute a monophonic syncopation. If N is a note on a certain instrument that precedes a note on a 181 | different instrument (Ndi), and Ndi has a metric weight greater than or equal to N, then the pair (N, Ndi) is said 182 | to constitute a polyphonic syncopation. 183 | """ 184 | 185 | # Metric profile as described by Witek et al. (2014) 186 | salience_w = [0, -3, -2, -3, -1, -3, -2, -3, -1, -3, -2, -3, -1, -3, -2, -3] 187 | syncopation_list = [] 188 | 189 | # number of time steps 190 | n = len(low_stream) 191 | 192 | # find pairs of N and Ndi notes events 193 | for ix in range(n): 194 | # describe the instruments present in current and next steps 195 | event = [low_stream[ix], mid_stream[ix], hi_stream[ix]] 196 | 197 | next_ix = (ix + 1) % n 198 | event_next = [ 199 | low_stream[next_ix], 200 | mid_stream[next_ix], 201 | hi_stream[next_ix], 202 | ] 203 | 204 | # syncopation occurs when adjacent events are different, and succeeding event has greater or equal metric weight 205 | if event != event_next and salience_w[next_ix] >= salience_w[ix]: 206 | # only process if there is a syncopation 207 | # analyze what type of syncopation is found to assign instrumental weight 208 | # instrumental weight depends on the relationship between the instruments in the pair 209 | 210 | instrumental_weight = None 211 | 212 | # Three-stream syncopation 213 | # Low against mid and hi 214 | if event[0] == 1 and event_next[1] == 1 and event_next[2] == 1: 215 | instrumental_weight = 2 216 | 217 | # Mid against low and high 218 | if event[1] == 1 and event_next[0] == 1 and event_next[2] == 1: 219 | instrumental_weight = 1 220 | 221 | # Two-stream syncopation 222 | # Low or mid against high 223 | if (event[0] == 1 or event[1] == 1) and event_next == [0, 0, 1]: 224 | instrumental_weight = 5 225 | 226 | # Low against mid (NOTE: not defined in [Witek et al., 2014]) 227 | if event == [1, 0, 0] and event_next == [0, 1, 0]: 228 | instrumental_weight = 2 229 | 230 | # Mid against low (NOTE: not defined in [Witek et al., 2014]) 231 | if event == [0, 1, 0] and event_next == [1, 0, 0]: 232 | instrumental_weight = 2 233 | 234 | local_syncopation = 0 235 | if instrumental_weight: 236 | local_syncopation = ( 237 | abs(salience_w[ix] - salience_w[next_ix]) + instrumental_weight 238 | ) 239 | syncopation_list.append(local_syncopation) 240 | 241 | return sum(syncopation_list) 242 | 243 | 244 | def poly_evenness(low_stream, mid_stream, hi_stream): 245 | """Compute the polyphonic evenness. Adapted from [Milne and Herff, 2020]""" 246 | low_evenness = evenness(low_stream) 247 | mid_evenness = evenness(mid_stream) 248 | hi_evenness = evenness(hi_stream) 249 | 250 | return low_evenness * 3 + mid_evenness * 2 + hi_evenness 251 | 252 | 253 | def poly_balance(low_stream, mid_stream, hi_stream): 254 | """Compute the polyphonic balance of a rhythm. Adapted from [Milne and Herff, 2020]""" 255 | 256 | d = density(low_stream) * 3 + density(mid_stream) * 2 + density(hi_stream) 257 | if d == 0: 258 | return 1 259 | 260 | center = np.array([0, 0]) 261 | iso_angle_16 = 2 * math.pi / 16 262 | 263 | Xlow = [3 * math.cos(i * iso_angle_16) for i, x in enumerate(low_stream) if x == 1] 264 | Ylow = [3 * math.sin(i * iso_angle_16) for i, x in enumerate(low_stream) if x == 1] 265 | matrixlow = np.array([Xlow, Ylow]) 266 | matrixlowsum = matrixlow.sum(axis=1) 267 | 268 | Xmid = [2 * math.cos(i * iso_angle_16) for i, x in enumerate(mid_stream) if x == 1] 269 | Ymid = [2 * math.sin(i * iso_angle_16) for i, x in enumerate(mid_stream) if x == 1] 270 | matrixmid = np.array([Xmid, Ymid]) 271 | matrixmidsum = matrixmid.sum(axis=1) 272 | 273 | Xhi = [2 * math.cos(i * iso_angle_16) for i, x in enumerate(hi_stream) if x == 1] 274 | Yhi = [2 * math.sin(i * iso_angle_16) for i, x in enumerate(hi_stream) if x == 1] 275 | matrixhi = np.array([Xhi, Yhi]) 276 | matrixhisum = matrixhi.sum(axis=1) 277 | 278 | matrixsum = matrixlowsum + matrixmidsum + matrixhisum 279 | 280 | magnitude = np.linalg.norm(matrixsum - center) / d 281 | 282 | return 1 - magnitude 283 | 284 | 285 | def poly_density(low_stream, mid_stream, hi_stream): 286 | # compute the total number of onsets 287 | return density(low_stream) + density(mid_stream) + density(hi_stream) 288 | --------------------------------------------------------------------------------