├── .gitignore ├── DESCRIPTION.rst ├── LICENCE.txt ├── MANIFEST.in ├── README.rst ├── data └── songs │ ├── amazing_grace.song │ ├── amazing_grace.song.mid │ ├── canon_riff.song │ ├── canon_riff.song.mid │ ├── canon_riff_tie_notes.song │ ├── canon_riff_tie_notes.song.mid │ ├── clementine.song │ ├── clementine.song.mid │ ├── silent_night.song │ ├── silent_night.song.mid │ ├── twinkle_twinkle_little_star.song │ ├── twinkle_twinkle_little_star.song.mid │ ├── yankee_doodle.song │ └── yankee_doodle.song.mid ├── melody_scripter ├── __init__.py ├── midi_song.py ├── play_song.py ├── song2midi.py └── song_parser.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── regression └── expected │ ├── amazing_grace.song.mid │ ├── amazing_grace.song.mid.dump │ ├── canon_riff.song.mid │ ├── canon_riff.song.mid.dump │ ├── canon_riff_tie_notes.song.mid │ ├── canon_riff_tie_notes.song.mid.dump │ ├── clementine.song.mid │ ├── clementine.song.mid.dump │ ├── silent_night.song.mid │ ├── silent_night.song.mid.dump │ ├── twinkle_twinkle_little_star.song.mid │ ├── twinkle_twinkle_little_star.song.mid.dump │ ├── yankee_doodle.song.mid │ └── yankee_doodle.song.mid.dump ├── test_regression.py └── test_song_parser.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Backup files 2 | *.~ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | bin/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | MANIFEST 26 | 27 | # Installer logs 28 | pip-log.txt 29 | pip-delete-this-directory.txt 30 | 31 | # Unit test / coverage reports 32 | .tox/ 33 | .coverage 34 | .cache 35 | nosetests.xml 36 | coverage.xml 37 | 38 | # Translations 39 | *.mo 40 | 41 | # Sphinx documentation 42 | docs/_build/ 43 | 44 | # .wav files generated from .mid files 45 | *.mid.wav 46 | 47 | # .mp3 files 48 | *.mp3 49 | 50 | tests/regression/output/ 51 | -------------------------------------------------------------------------------- /DESCRIPTION.rst: -------------------------------------------------------------------------------- 1 | Melody Scripter 2 | =============== 3 | 4 | *Melody Scripter* is a Python library which reads tunes written in a simple 5 | sequential format consisting of notes, bar lines and chords, with optional 6 | prelude lines specifying song and track properties. 7 | 8 | Currently the only functionality of *Melody Scripter* is to output Midi files. 9 | 10 | However, it has been written with the intention to facilitate the analysis 11 | of musical items, providing a relatively straightforward object model 12 | of a tune and the components of the tune. 13 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Philip Dorrell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include DESCRIPTION.rst 2 | 3 | # Include the test suite (FIXME: does not work yet) 4 | # recursive-include tests * 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. |--| unicode:: U+2013 .. en dash 2 | 3 | Melody Scripter and MelodyScript 4 | ================================ 5 | 6 | **MelodyScript** is a melody-oriented DSL for describing melodies, which 7 | can be optionally annotated with chords. 8 | 9 | **Melody Scripter** parses a **MelodyScript** file into a Python **Song** object 10 | model, which can be used to generate a Midi file. 11 | 12 | Here is "Yankee Doodle" in MelodyScript:: 13 | 14 | *song: tempo_bpm=200, time_signature=4/4, ticks_per_beat=4 15 | 16 | *track.melody: instrument=73, volume=120, octave=5 17 | *track.chord: instrument=40, volume= 50, octave=3 18 | *track.bass: instrument=19, volume=100, octave=2 19 | 20 | [C] c c d e | [C] c e [G7] d g, | [C] c c d e | [C] c2 [G7] b | 21 | [C] c c d e | [F] f e d c | [G7] b g a b | [C] c2 c1 [] r1 | 22 | [F] a. bh a1 g | [F] a b c2 | [C] g. ah g1 f | [C] e2 g2 | 23 | [F] a. bh a1 g | [F] a b c a | [G7] g c b d | [C] c2 c1 r1 24 | 25 | In MelodyScript, a "melody" consists of a sequence 26 | of notes on the standard Western musical scale, together with bar lines 27 | (which must match the specified time signature) and chords, with optional 28 | bass notes where different from the chord root note. 29 | 30 | The Song object model can generate a Midi file for the melody, for example: 31 | `yankee_doodle.song.mid `_. 32 | You can listen to an audio version of that midi file (rendered using **fluidsynth**) at http://whatismusic.info/melody_scripter/. 33 | 34 | Currently Midi output is the only functionality provided by the Song object model, 35 | but the object model would also provide a convenient representation of melody information 36 | for the purposes of scientific analysis. 37 | 38 | MelodyScript Syntax 39 | =================== 40 | 41 | MelodyScript is line-oriented. There are two types of lines: 42 | 43 | * **Command** lines. Any line starting with the character ``*`` (with possibly 44 | preceding whitespace) is parsed as a command line. 45 | * **Song Item** lines. All other lines are parsed as sequences of whitespace-separated Song Items. 46 | 47 | Command Lines 48 | ------------- 49 | 50 | A command line starts with a **command** (after the initial ``*`` character). 51 | 52 | The command is followed by a ``:`` character, and then one or more 53 | comma-separated arguments. 54 | 55 | Currently there are five commands, which are ``song``, three 'track' commands: 56 | ``track.melody``, ``track.chord`` and ``track.bass``, and ``groove``. 57 | 58 | The ``song`` and ``track`` commands take arguments which are **property settings**, consisting 59 | in each case of a key and a value, specified as *key* = *value*. 60 | 61 | Currently all property settings can be executed only prior to any song items, 62 | but in future they may be allowed during the song (or additional commands may 63 | be defined which are valid within the song). 64 | 65 | Song Property Settings 66 | ---------------------- 67 | 68 | Available property settings for the ``song`` command are: 69 | 70 | +-------------------+--------------------------------------+------------+----------------------------------+ 71 | | key | value | default | valid values | 72 | +===================+======================================+============+==================================+ 73 | | tempo_bpm | Tempo in beats per minute | 120 | 1 to 1000 | 74 | +-------------------+--------------------------------------+------------+----------------------------------+ 75 | | time_signature | Example: 3/4 | 4/4 | Any integer / (1,2,4,8,16 or 32) | 76 | +-------------------+--------------------------------------+------------+----------------------------------+ 77 | | ticks_per_beat | Ticks per beat | 4 | 1 to 2000 | 78 | +-------------------+--------------------------------------+------------+----------------------------------+ 79 | | subticks_per_tick | Ticks per beat | 1 | 1 to 100 | 80 | +-------------------+--------------------------------------+------------+----------------------------------+ 81 | | tranpose | Transposition (in semitones) | 0 | -127 to 127 | 82 | +-------------------+--------------------------------------+------------+----------------------------------+ 83 | 84 | The ``tempo_bpm`` and ``ticks_per_beat`` values both determine corresponding values when 85 | a Midi file is generated. "Ticks" are the unit of time in the song, and every note 86 | or rest length must be a whole number of ticks |--| if not, an error occurs. 87 | 88 | The ``time_signature`` is the standard time signature used to describe a bar, when a **numerator** that 89 | specifies the number of *beats* per bar, and a **denominator** that specifies the length of one beat as a 90 | function of a whole note. (To keep things simple, the ``ticks_per_beat`` value must be defined so that 91 | one crotchet contains a whole number of ticks.) 92 | 93 | If the contents of a bar do not have the correct total length, it's an error. 94 | (It's OK to have partial bars at the start and end of the song.) 95 | 96 | The ``subticks_per_tick`` value is only relevant to the ``groove`` command, and it determines 97 | how many "subticks" there are in each tick. ("Groove" is defined in terms of sub-tick displacements.) 98 | 99 | The ``transpose`` value raises or lowers all the note values when generating the Midi file. (An error will occur 100 | if transposition causes a note to go out of the valid range of 0 to 127.) 101 | 102 | 103 | Track Property Settings 104 | ----------------------- 105 | 106 | The three tracks, **melody**, **chord** and **bass**, correspond to three Midi tracks generated in the Midi output file. 107 | Each track has its own settings: 108 | 109 | +----------------+--------------------------------------+------------+--------------+ 110 | | key | value | default | valid values | 111 | +================+======================================+============+==============+ 112 | | instrument | Midi instrument number | 0 | 0 to 127 | 113 | +----------------+--------------------------------------+------------+--------------+ 114 | | volume | Midi volume (velocity) | 0 | 0 to 127 | 115 | +----------------+--------------------------------------+------------+--------------+ 116 | | octave | Octave for initial melody note, and | 3, 1, 0 | -1 to 10 | 117 | | | for all chord root notes and all | | | 118 | | | bass notes. | | | 119 | +----------------+--------------------------------------+------------+--------------+ 120 | 121 | (The octave defaults are for **melody**, **chord** and **bass** respectively.) 122 | 123 | The ``instrument`` and ``volume`` settings define the Midi settings for each track. Midi instrument numbers 124 | range from 0 to 127, and the actual sounds depend on the SoundFont used to play the Midi song, 125 | although there is a standard **GM** set of Midi instruments definitions (where the default of **0** 126 | corresponds to Acoustic Grand Piano). 127 | 128 | Currently MelodyScript does not have any provision for per-note volume (velocity) specification. In 129 | practice there is no easy way to determine appropriate volume values, for example when typing in from 130 | sheet music. For playback it is recommended to choose suitable instrument sounds that work well with 131 | constant volume (for example see choices made in the sample song files in this project). 132 | 133 | The ``octave`` setting determines which Midi octave the first melody note belongs to, and for 134 | the **chord** and **bass** tracks, it determines the octave of all root notes and bass notes respectively. 135 | (Melody note octave values are determined relatively, as will be described in the Song Items section next.) 136 | 137 | Although octave values are allowed from -1 to 10, not all Midi notes in the 10th octave are allowed, 138 | and an error will occur if a note occurs with a value greater than 127. 139 | 140 | Groove 141 | ------ 142 | 143 | A ``groove`` command is specified by one or more numerical "sub-tick" displacements. 144 | 145 | The number of values given in a ``groove`` command must divide evenly into the number of ticks in the bar. 146 | 147 | For example, a ``groove`` command might specify ``0 2 1 2`` where there are 4 beats per bar and 2 ticks 148 | per beat. The 4 groove values are applied to the 8 tick values per bar by repeating them twice, ie 149 | **0 2 1 2 0 2 1 2**. Each value determines how many sub-ticks are added to the time of each corresponding 150 | tick in each bar. 151 | 152 | The ``subticks_per_tick`` value in the ``song`` command specifies the length of a sub-tick. So if there 153 | are 10 subticks per tick, then a groove value of 2 corresponds to a displacement of 2/10 of tick. 154 | 155 | Song Items 156 | ---------- 157 | 158 | There are six types of song item that can be parsed: 159 | 160 | * Note 161 | * Tie 162 | * Rest 163 | * Chord 164 | * Bar Line 165 | * Cut 166 | 167 | All song items are represented by tokens that don't contain any whitespace, and song items in a line must 168 | be separated from each other by whitespace. 169 | 170 | 171 | Notes 172 | ----- 173 | 174 | The components of a note are, in order: 175 | 176 | Continued marker: 177 | If provided, specified as ``~``. This indicates that a note is a continuation 178 | of the previous note. 179 | Note letter: 180 | A lower case letter from ``a`` to ``g``. For the purposes of defining an octave, 181 | the octave starts at ``c`` (this is a standard convention). 182 | Sharp or flat: 183 | Represented by ``+`` or ``-``, and only one is allowed. 184 | Ups or downs (octave adjustments): 185 | If provided, specified as one or more ``'`` for up, or one or more ``,`` for down. 186 | Duration: 187 | If note duration is not specified, then it is given a default value. For the first 188 | note in the melody, and the first note in each bar, the default duration is 1 crotchet 189 | (ie one 'quarter note'). 190 | For all other notes, the default duration is the duration of the previous note. 191 | If a duration is specified, then the specification consists of the following 192 | components: 193 | 194 | * The initial number of crotchets (if not given, this defaults to 1). 195 | 196 | * ``h`` or ``q`` qualifiers, possibly repeated, which multiply the duration 197 | by a half or a quarter respectively. 198 | 199 | * ``t`` qualifier (at most once), which multiplies the duration by a third 200 | 201 | * ``.`` qualifier (at most once) which multiplies the duration by 3/2 202 | 203 | At least one component must be given, otherwise the previously described default 204 | value applies. 205 | 206 | Any note duration must 207 | be a whole number of ticks, and an error will occur if a note length is defined 208 | which is a fractional number of ticks. (In such a case, if the note length is 209 | correct, you will need to increase or change the specified ``ticks_per_beat`` 210 | song property.) 211 | 212 | To-be-continued marker: 213 | If provided, specified as ``~``. This indicates that a note will be continued 214 | by the next note. 215 | 216 | Except for the very first note, MelodyScript does not provide for each note to 217 | specify its octave. Instead, pitch values are specified relative to the previous note. 218 | If no "up" or "down" markers are specified, the rule is to always choose the closest 219 | possibility. If this choice is ambiguous, eg when going from ``f`` to ``b`` or vice versa, then an error occurs. 220 | 221 | If one up or one down is specified, then the next note should be the first note matching 222 | the given note letter, above 223 | or below the previous note, respectively. If more than one up or down marker is given, 224 | then go an extra octave up or down for each extra marker. 225 | 226 | So, for example, ``c`` followed by ``e`` means go up to the next E, and ``c`` followed 227 | by ``e'`` *also* means go up to the next E. Whereas ``e''`` means go up 9 notes to the E 228 | above that, ``e,`` means go down to the first E below, and ``e,,`` means go to the E 229 | below that one. 230 | 231 | (The up and down markers are the same as used in LilyPond in relative mode, however the rule of 232 | interpretation is different |--| in MelodyScript one ``'`` always means the next note up 233 | from the previous note, and similarly one ``,`` means the next note down. 234 | Also, in MelodyScript the rule defines "closest" based on the the actual semitone values of 235 | the previous and current notes as specified by letter and optional sharps or flats, whereas 236 | LilyPond applies a rule that ignores sharps and flats.) 237 | 238 | Ties, and Note Continuations 239 | ---------------------------- 240 | 241 | A **continuation** is where one note is represented by the joining of two or more 242 | note items in the melody script. Because bar lines have to occur in the right place, 243 | notes that cross bar lines *have* to be represented using continuations. There may 244 | also be some note lengths that cannot be represented using the Duration format 245 | specified above, so they have to be constructed from multiple notes joined together. 246 | 247 | In other situations, the use of continuations is optional. 248 | 249 | There are two ways to specify that one note is to be continued by a second note: 250 | 251 | * Either, the first note ends with ``~`` and the second note starts with ``~``, 252 | * Or, a ``~`` **Tie** item occurs between the two notes. 253 | 254 | It is possible for more than two notes to form a continuation |--| the 255 | required joinings need to be indicated in each case. This would be necessary, 256 | for example, to specify a note that filled more than two bars. 257 | 258 | Rests 259 | ----- 260 | 261 | A **Rest** consists of the letter ``r`` followed by a duration specification. The duration 262 | specification for rests is very similar to that for notes, but there is no default 263 | duration, and at least one part of the duration specification must be given. If 264 | only qualifiers are given, then they are applied to a value of 1. So, for example, 265 | ``rh`` is a valid rest, representing half a crotchet, ie a quaver. 266 | 267 | Chords 268 | ------ 269 | 270 | **Chords** are specified by enclosing their contents in ``[`` and ``]``. Currently there 271 | are two formats: 272 | 273 | Root note plus descriptor 274 | The root note is given as an upper-case letter with an optional ``+`` or ``-`` for sharp or flat, 275 | and one of several standard "descriptors" from empty "" (for a major chord), ``7``, ``m``, 276 | ``m7`` and ``maj7``. So, for example, ``[Cm]`` represents a C minor chord. 277 | Root note plus other chord notes. 278 | Prefixed with a ``:``, the notes are given as upper-case letters with optional ``+``/``-`` sharp 279 | or flat, with the root note first. So, for example, ``[:CE-G]`` represents a C minor chord. 280 | 281 | In each case, chords may contain an optional bass note specifier, to specify a bass note 282 | different from the root note. This is given as a ``/`` character, followed by an upper-case 283 | letter and optional sharp or flat. So, for example, ``[A+m/F+]`` represents A sharp minor 284 | with an F sharp bass. 285 | 286 | Bar Lines 287 | --------- 288 | 289 | **Bar Lines** are represented by ``|``. Bar lines are used to check that the total lengths of notes 290 | and rests in each bar have the correct values. They also reset the default note 291 | duration to 1 crotchet. Bar lines do not have any direct effect on Midi output. 292 | 293 | Cuts 294 | ---- 295 | 296 | A **Cut** is represented by ``!``. **Cut** means "cut out all previous song items". A Cut 297 | is useful when editing, when you want to play part of the song without starting all the way from the beginning. 298 | (There would not normally be any reason to include a Cut in a completed song.) 299 | 300 | 301 | Compilation and Playback 302 | ======================== 303 | 304 | The ``main()`` method of ``song2midi.py`` takes one argument which is the name of a MelodyScript file, 305 | and compiles it into a Midi file, using the name of the input file with ``.mid`` appended. This Python 306 | module is also made available as a console script **song2midi** when MelodyScripter is installed 307 | into a Python environment. 308 | 309 | For example:: 310 | 311 | > song2midi yankee_doodle.song 312 | 313 | Compiling song file yankee_doodle.song to yankee_doodle.song.mid ... 314 | Writing midi to yankee_doodle.song.mid ... 315 | Successfully wrote midi file yankee_doodle.song.mid. 316 | 317 | 318 | The ``main()`` method of ``play_song.py`` generates the same Midi file as ``song2midi.py`` and, 319 | after generating the Midi file, plays it using the ``/usr/bin/cvlc`` command, if that command is available. 320 | **cvlc** is the command line version of VLC, as installed on an Linux system, and it only plays Midi files 321 | if the **vlc-plugin-fluidsynth** VLC plugin is installed. This module is also available as the **play_song** 322 | console script. 323 | 324 | For example:: 325 | 326 | > play_song yankee_doodle.song 327 | 328 | Playing song yankee_doodle.song (after compiling to yankee_doodle.song.mid) ... 329 | Writing midi to yankee_doodle.song.mid ... 330 | Playing midi file yankee_doodle.song.mid with cvlc ... 331 | VLC media player 2.1.6 Rincewind (revision 2.1.6-0-gea01d28) 332 | [0x89f45a0] dummy interface: using the dummy interface module... 333 | fluidsynth: warning: Failed to pin the sample data to RAM; swapping is possible. 334 | 335 | (Console output from **cvlc** and the fluidsynth plug-in may be different on your system.) 336 | 337 | An alternative playback option on Linux is **timidity**, however even with the ``--output-24bit`` 338 | option, on my system, the sound quality is poor at the beginning of the song. 339 | 340 | 341 | Limitations and Installation Issues 342 | =================================== 343 | 344 | To install the latest stable version of **Melody Scripter** into a Python environment, execute:: 345 | 346 | pip install https://github.com/pdorrell/melody_scripter/archive/master.zip --process-dependency-links 347 | 348 | Notes: 349 | 350 | * **Melody Scripter** depends on the `midi 0.2.3 `_ library 351 | to write files. **midi 0.2.3** only runs on Python 2.x, and on Linux, the installation requires 352 | that **swig** be installed (eg by ``sudo apt-get install swig``). So, for the moment, **Melody Scripter** 353 | has the same limitations, and it is only tested to run on Python 2.7. 354 | 355 | * ``--process-dependency-links`` is currently required because the **midi 0.2.3** dependency is not 356 | directly downloadable from Pypi. 357 | 358 | * To install a specific tagged release, eg **0.0.5**, replace 'master' in the URL above with the tag. 359 | 360 | 361 | 362 | -------------------------------------------------------------------------------- /data/songs/amazing_grace.song: -------------------------------------------------------------------------------- 1 | *song: tempo_bpm=100, time_signature=3/4, ticks_per_beat=4 2 | *track.melody: instrument = 73, volume=80, octave=4 3 | *track.chord: instrument =40, volume=50,octave=3 4 | *track.bass: instrument=19, volume=120, octave=2 5 | 6 | gh c | [C] c2 eq d ch | [C] e2 [C7] d1 | [F] c2 a1 | [C] g2 7 | gh c | [C] c2 eq d ch | e2 dh e | [G] g2 8 | eh g | [G] g2 eq d ch | [C] e2 [C7] d1 | [F] c2 a1 | [C] g2 9 | gh c | [C] c2 eq d ch | [C] e3 | [G7] d3 | [C] c3 10 | -------------------------------------------------------------------------------- /data/songs/amazing_grace.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/data/songs/amazing_grace.song.mid -------------------------------------------------------------------------------- /data/songs/canon_riff.song: -------------------------------------------------------------------------------- 1 | *song: tempo_bpm=150, time_signature=4/4, ticks_per_beat=4 2 | *track.melody: instrument = 73, volume=120, octave=4 3 | *track.chord: instrument =40, volume=50,octave=3 4 | *track.bass: instrument=19, volume=100, octave=2 5 | 6 | [C] c e e c | [G] d g g d | [Am] c c c a | [Em] b e e b | [F] a c c a | [C] g c c g | [F] a c c d | [G] d2 r2 | 7 | 8 | [C] e d ch d e1 | [G] d c b2 | [Am] c b ah b c1 | [Em] b a g2 | [F] a g fh gh a1 | [C] g f e g | [F] a g ah b c1 | [G] d3 9 | -------------------------------------------------------------------------------- /data/songs/canon_riff.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/data/songs/canon_riff.song.mid -------------------------------------------------------------------------------- /data/songs/canon_riff_tie_notes.song: -------------------------------------------------------------------------------- 1 | *song: tempo_bpm=170, time_signature=4/4, ticks_per_beat=4 2 | *track.melody: instrument = 73, volume=120, octave=4 3 | *track.chord: instrument =40, volume=50,octave=3 4 | *track.bass: instrument=19, volume=100, octave=2 5 | 6 | [C] c e e c | [G] d g g d | [Am] c c c a | [Em] b e e b | [F] a c c a | [C] g c c g | [F] a c c d | [G] d2 r2 | 7 | 8 | [C] e d ch d eh dh~ | [G] ~d c b2 | [Am] c b ah b ch bh~ | [Em] ~bh~ ~bh a1 g2 | [F] a g fh gh a1 | [C] g f eh ~ eh g1 | [F] a g ah b c1 | [G] d ~ d~ ~d 9 | -------------------------------------------------------------------------------- /data/songs/canon_riff_tie_notes.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/data/songs/canon_riff_tie_notes.song.mid -------------------------------------------------------------------------------- /data/songs/clementine.song: -------------------------------------------------------------------------------- 1 | *song: tempo_bpm=100, time_signature=3/4, ticks_per_beat=4 2 | *track.melody: instrument = 73, volume=120, octave=5 3 | *track.chord: instrument =40, volume=50,octave=3 4 | *track.bass: instrument=19, volume=100, octave=2 5 | 6 | ch. cq | [C] c g e'h. eq | [C] e c ch. eq | [C] g. gh f e | [G7] d2 7 | 8 | dh. eq | [F] f f eh. dq | [C] e c ch. eq | [G7] d. g,h bh. dq | [C] c2 9 | -------------------------------------------------------------------------------- /data/songs/clementine.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/data/songs/clementine.song.mid -------------------------------------------------------------------------------- /data/songs/silent_night.song: -------------------------------------------------------------------------------- 1 | *song: tempo_bpm=110, time_signature=3/4, ticks_per_beat=2, subticks_per_tick=4 2 | 3 | *groove: 0 1 4 | 5 | *track.melody: instrument=73, volume=120, octave=4 6 | *track.chord: instrument=40, volume= 50, octave=3 7 | *track.bass: instrument=19, volume=100, octave=2 8 | 9 | | [F] c. dh c1 | a2 r1 | [F] c. dh c1 | a2 r1 10 | | [C] g'2 g1 | e2 r1 | [F] f2 f1 | c2 r1 11 | | [B-] d2 d1 | f. eh d1 | [F] c. dh c1 | a2 r1 12 | | [B-] d2 d1 | f. eh d1 | [F] c. dh c1 | a2 r1 13 | | [C] g'2 g1 | b-. gh e1 | [F] f3 | a2 r1 14 | | f c a | [C] c. b-h g1 | [F] f2 15 | -------------------------------------------------------------------------------- /data/songs/silent_night.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/data/songs/silent_night.song.mid -------------------------------------------------------------------------------- /data/songs/twinkle_twinkle_little_star.song: -------------------------------------------------------------------------------- 1 | *song: tempo_bpm=60, time_signature=4/4, ticks_per_beat=4 2 | *track.melody: instrument = 73, volume=120, octave=4 3 | *track.chord: instrument =40, volume=50,octave=3 4 | *track.bass: instrument=19, volume=100, octave=2 5 | 6 | [C] ch c g' g [F] a a [C] g1 | [F] fh f [C] e e [G] d d [C] c1 | 7 | [C] g'h g [F] f f [C] e e [G] d1 | [C] g'h g [F] f f [C] e e [G] d1 | 8 | [C] ch c g' g [F] a a [C] g1 | [F] fh f [C] e e [G] d d [C] c1 | 9 | 10 | -------------------------------------------------------------------------------- /data/songs/twinkle_twinkle_little_star.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/data/songs/twinkle_twinkle_little_star.song.mid -------------------------------------------------------------------------------- /data/songs/yankee_doodle.song: -------------------------------------------------------------------------------- 1 | *song: tempo_bpm=200, time_signature=4/4, ticks_per_beat=4 2 | 3 | *track.melody: instrument=73, volume=120, octave=5 4 | *track.chord: instrument=40, volume= 50, octave=3 5 | *track.bass: instrument=19, volume=100, octave=2 6 | 7 | [C] c c d e | [C] c e [G7] d g, | 8 | [C] c c d e | [C] c2 [G7] b | 9 | [C] c c d e | [F] f e d c | 10 | [G7] b g a b | [C] c2 c1 [] r1 | 11 | 12 | [F] a. bh a1 g | [F] a b c2 | 13 | [C] g. ah g1 f | [C] e2 g2 | 14 | [F] a. bh a1 g | [F] a b c a | 15 | [G7] g c b d | [C] c2 c1 r1 16 | -------------------------------------------------------------------------------- /data/songs/yankee_doodle.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/data/songs/yankee_doodle.song.mid -------------------------------------------------------------------------------- /melody_scripter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/melody_scripter/__init__.py -------------------------------------------------------------------------------- /melody_scripter/midi_song.py: -------------------------------------------------------------------------------- 1 | import midi 2 | 3 | from song_parser import Song, FileToParse, ParseException 4 | 5 | import subprocess, os, re, sys 6 | 7 | class MidiTrack(object): 8 | """A track in a song, also, the owner of one channel""" 9 | 10 | def __init__(self, midi_song, track, name, track_number, channel_number): 11 | self.midi_song = midi_song 12 | self.song = midi_song.song 13 | self.transpose = self.song.transpose 14 | self.track = track 15 | self.track_number = track_number 16 | self.channel_number = channel_number 17 | self.id = "%s/%s" % (track_number, channel_number) 18 | self.name = name 19 | self.volume = track.volume 20 | self.midi_data = midi_song.midi_data 21 | self.midi_data_track = midi.Track() 22 | self.midi_data.append(self.midi_data_track) 23 | self.set_tempo_bpm(0, self.song.tempo_bpm) 24 | self.groove = self.song.groove 25 | self.set_instrument(0, self.track.instrument) 26 | self.initial_delay_subticks = midi_song.initial_delay_subticks 27 | 28 | def set_tempo_bpm(self, time, tempo): 29 | #print(" %s setting tempo at %s to %s bpm" % (self.id, time, tempo)) 30 | self.midi_data_track.append(midi.SetTempoEvent(tick = time, bpm = tempo)) 31 | 32 | def add_note(self, midi_note, time, duration): 33 | #print(" %s playing note %s at time %s, %s ticks" % (self.id, midi_note, time, duration)) 34 | start_time = self.groove.get_subticks(time) + self.initial_delay_subticks 35 | self.midi_data_track.append(midi.NoteOnEvent(tick = start_time, channel = self.channel_number, 36 | pitch = midi_note, velocity = self.volume)) 37 | end_time = self.groove.get_subticks(time+duration) + self.initial_delay_subticks 38 | self.midi_data_track.append(midi.NoteOffEvent(tick = end_time, channel = self.channel_number, 39 | pitch = midi_note)) 40 | 41 | def add_notes(self, midi_notes, time, duration): 42 | #print(" %s playing notes %r at time %s, %s ticks" % (self.id, midi_notes, time, duration)) 43 | start_time = self.groove.get_subticks(time) + self.initial_delay_subticks 44 | for midi_note in midi_notes: 45 | self.midi_data_track.append(midi.NoteOnEvent(tick = start_time, channel = self.channel_number, 46 | pitch = midi_note, velocity = self.volume)) 47 | end_time = self.groove.get_subticks(time+duration) + self.initial_delay_subticks 48 | for midi_note in midi_notes: 49 | self.midi_data_track.append(midi.NoteOffEvent(tick = end_time, channel = self.channel_number, 50 | pitch = midi_note)) 51 | 52 | def set_instrument(self, time, instrument_number): 53 | #print(" %s set instrument at %s to %s" % (self.id, time, instrument_number)) 54 | self.midi_data_track.append(midi.ProgramChangeEvent(channel = self.channel_number, 55 | tick = time, value = instrument_number)) 56 | 57 | def render(self): 58 | for item in self.track.items: 59 | item.visit_midi_track(self) 60 | self.midi_data_track.make_ticks_rel() 61 | self.midi_data_track.append(midi.EndOfTrackEvent(tick=1)) 62 | 63 | class MidiSong(object): 64 | 65 | def __init__(self, song, initial_delay_seconds = 0): 66 | 67 | self.song = song 68 | self.tempo_bpm = song.tempo_bpm 69 | self.groove = song.groove 70 | self.midi_tracks = {} 71 | self.initial_delay_subticks = int(round(initial_delay_seconds * song.subticks_per_second)) 72 | 73 | def render(self): 74 | self.midi_data = midi.Pattern(resolution = self.song.ticks_per_beat * self.song.subticks_per_tick) 75 | 76 | for name, track in self.song.tracks.items(): 77 | midi_track = self.add_midi_track(name, name, track) 78 | midi_track.render() 79 | # print self.midi_data 80 | 81 | def add_midi_track(self, key, name, track): 82 | next_track_number = len(self.midi_tracks) 83 | midi_track = MidiTrack(self, track, name, next_track_number, next_track_number) 84 | self.midi_tracks[key] = midi_track 85 | return midi_track 86 | 87 | def write_midi_file(self, file_name): 88 | #print("Rendering midi data...") 89 | self.render() 90 | print("Writing midi to %s ..." % file_name) 91 | midi.write_midifile(file_name, self.midi_data) 92 | 93 | def play_midi_file_with_cvlc(file_name): 94 | print("Playing midi file %s with cvlc ..." % file_name) 95 | cvlc_path = '/usr/bin/cvlc' 96 | if os.path.exists(cvlc_path): 97 | subprocess.call([cvlc_path, file_name, 'vlc://quit']) 98 | else: 99 | print("Cannot play midi file, %s does not exist on your system" % cvlc_path) 100 | 101 | def play_midi_file_with_timidity(file_name): 102 | subprocess.call(['/usr/bin/timidity', '--output-24bit', file_name]) 103 | 104 | def dump_midi_file(file_name): 105 | pattern = midi.read_midifile(file_name) 106 | return repr(pattern) 107 | 108 | def compile_to_midi(song_file_path, midi_file_name, initial_delay_seconds = 0): 109 | song = Song.parse(FileToParse(song_file_path)) 110 | midi_song = MidiSong(song, initial_delay_seconds = initial_delay_seconds) 111 | midi_song.write_midi_file(midi_file_name) 112 | 113 | 114 | def play_song(song_file_path): 115 | midi_file_name = "%s.mid" % song_file_path 116 | print("Playing song %s (after compiling to %s) ..." % (song_file_path, midi_file_name)) 117 | try: 118 | compile_to_midi(song_file_path, midi_file_name, initial_delay_seconds = 0.2) 119 | play_midi_file_with_cvlc(midi_file_name) 120 | #play_midi_file_with_timidity(midi_file_name) 121 | # dump_midi_file(midi_file_name) 122 | except ParseException, pe: 123 | pe.show_error() 124 | 125 | def main(): 126 | # Play a sample song 127 | song_file_name = 'canon_riff.song' 128 | song_file_path = os.path.join(os.path.dirname(__file__),'..', 'data', 'songs', song_file_name) 129 | play_song(song_file_path) 130 | 131 | if __name__ == "__main__": 132 | main() 133 | -------------------------------------------------------------------------------- /melody_scripter/play_song.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from midi_song import play_song 4 | 5 | def main(): 6 | if len(sys.argv) == 2: 7 | song_file_path = sys.argv[1] 8 | play_song(song_file_path) 9 | else: 10 | print("Useage: %s " % sys.argv[0]) 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /melody_scripter/song2midi.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from midi_song import compile_to_midi 4 | 5 | def main(): 6 | if len(sys.argv) == 2: 7 | song_file_path = sys.argv[1] 8 | midi_file_path = "%s.mid" % song_file_path 9 | print("Compiling song file %s to %s ..." % (song_file_path, midi_file_path)) 10 | compile_to_midi(song_file_path, midi_file_path, initial_delay_seconds = 0.2) 11 | print("Successfully wrote midi file %s." % midi_file_path) 12 | else: 13 | print("Useage: %s " % sys.argv[0]) 14 | 15 | if __name__ == "__main__": 16 | main() 17 | -------------------------------------------------------------------------------- /melody_scripter/song_parser.py: -------------------------------------------------------------------------------- 1 | import subprocess, os, regex, sys 2 | from contextlib import contextmanager 3 | 4 | DIATONIC_OFFSETS = [0, 2, 4, 5, 7, 9, 11] 5 | 6 | CHORD_DESCRIPTOR_OFFSETS = {'': [0, 4, 7], 7 | '7': [0, 4, 7, 10], 8 | 'm': [0, 3, 7], 9 | 'm7': [0, 3, 7, 10], 10 | 'maj7': [0, 4, 7, 11]} 11 | 12 | NOTE_NAMES_LOWER_CASE = "cdefgab" 13 | 14 | NOTE_NAMES_UPPER_CASE = "CDEFGAB" 15 | 16 | DEFAULT_DURATION = (1, 1) 17 | 18 | def scale_number_from_letter(letter): 19 | ord_letter = ord(letter) 20 | if ord_letter > 96: 21 | offset_from_c = ord(letter) - 99 22 | else: 23 | offset_from_c = ord(letter) - 67 24 | return offset_from_c if offset_from_c >= 0 else offset_from_c + 7 25 | 26 | def valid_midi_note(midi_note): 27 | if midi_note < 0: 28 | raise ParseException('Note number %s < 0' % midi_note) 29 | if midi_note > 127: 30 | raise ParseException('Note number %s > 127' % midi_note) 31 | return midi_note 32 | 33 | @contextmanager 34 | def parse_source(source): 35 | try: 36 | yield 37 | except ParseException, pe: 38 | if pe.location is None: 39 | pe.location = source 40 | raise pe 41 | 42 | class ParseException(Exception): 43 | def __init__(self, message, location = None): 44 | super(Exception, self).__init__(message) 45 | self.location = location 46 | 47 | def show_error(self): 48 | if self.location is None: 49 | raise Exception('No location given for ParseException (with message %s)' % self.message) 50 | self.location.show_error(self.message) 51 | 52 | class ParseLeftOverException(ParseException): 53 | def __init__(self, leftover): 54 | super(ParseLeftOverException, self).__init__('Extra data when parsing: %r' % leftover.value, leftover) 55 | 56 | class FileToParse(object): 57 | def __init__(self, file_name): 58 | self.file_name = file_name 59 | 60 | def read_lines(self): 61 | line_number = 0 62 | with open(self.file_name, "r") as f: 63 | for line in f.readlines(): 64 | line = line.rstrip('\n') 65 | line_number += 1 66 | yield LineToParse(self, line_number, line).as_region() 67 | 68 | class StringToParse(object): 69 | def __init__(self, file_name, lines_string): 70 | self.file_name = file_name 71 | self.lines = lines_string.split("\n") 72 | 73 | def read_lines(self): 74 | line_number = 0 75 | for line in self.lines: 76 | line_number += 1 77 | yield LineToParse(self, line_number, line).as_region() 78 | 79 | 80 | class LineToParse(object): 81 | def __init__(self, file_to_parse, line_number, line): 82 | self.file_to_parse = file_to_parse 83 | self.line_number = line_number 84 | self.line = line 85 | 86 | def as_region(self): 87 | return LineRegionToParse(self, 0, len(self.line)) 88 | 89 | def show_error(self, error_message, pos): 90 | print("") 91 | print("%s:%s:%s" % (self.file_to_parse.file_name, self.line_number, error_message)) 92 | print("") 93 | print(self.line) 94 | indent = " " * pos 95 | print("%s^" % indent) 96 | print("%s%s" % (indent, error_message)) 97 | print("") 98 | 99 | class RegexFailedToMatch(Exception): 100 | 101 | def __init__(self): 102 | super(RegexFailedToMatch, self).__init__('Failed to match regex') 103 | 104 | class LineRegionToParse(object): 105 | def __init__(self, line_to_parse, start, end, regex = None, match = None): 106 | self.line_to_parse = line_to_parse 107 | self.start = start 108 | self.end = end 109 | self.value = self.line_to_parse.line[start:end] 110 | 111 | def parse(self, regex): 112 | match = self.match(regex) 113 | if match: 114 | match_end = match.end() 115 | if match_end < self.end: 116 | raise ParseLeftOverException(self.sub_region((match_end, self.end))) 117 | self.regex = regex 118 | self.match = match 119 | self.match_groupdict = match.groupdict() if match else None 120 | else: 121 | raise RegexFailedToMatch() 122 | 123 | def rest_of_line(self): 124 | return self.line_to_parse.line[self.start:] 125 | 126 | def sub_region(self, span): 127 | start, end = span 128 | return LineRegionToParse(self.line_to_parse, start, end) 129 | 130 | def match(self, regex): 131 | return regex.match(self.line_to_parse.line, self.start, self.end) 132 | 133 | def named_groups(self, name): 134 | group_index = self.regex.groupindex[name] 135 | capture_spans = self.match.spans(group_index) 136 | return [self.sub_region(span) for span in capture_spans] 137 | 138 | def named_group(self, name): 139 | if self.match_groupdict[name] is not None: 140 | group_index = self.regex.groupindex[name] 141 | return LineRegionToParse(self.line_to_parse, self.match.start(group_index), self.match.end(group_index)) 142 | else: 143 | return None 144 | 145 | def show_error(self, error_message): 146 | self.line_to_parse.show_error(error_message, self.start) 147 | 148 | strip_regex = regex.compile(r'\s*(?P.*\S)\s*') 149 | 150 | def stripped(self): 151 | self.parse(self.strip_regex) 152 | return self.named_group('stripped') 153 | 154 | class Parseable(object): 155 | 156 | def __eq__(self, other): 157 | return (other is not None and self.__class__ == other.__class__ and self.as_data() == other.as_data()) 158 | 159 | def __ne__(self, other): 160 | return not (self == other) 161 | 162 | def __hash__(self): 163 | return hash(self.__class__) + hash(self.as_data()) 164 | 165 | def __repr__(self): 166 | return "%s:%r" % (self.__class__.__name__, self.as_data()) 167 | 168 | class ParseableFromRegex(Parseable): 169 | 170 | @classmethod 171 | def parse(cls, region): 172 | try: 173 | region.parse(cls.parse_regex) 174 | except ParseLeftOverException, pleo: 175 | raise ParseException('Invalid %s: "%s" (extra data "%s")' 176 | % (cls.description, region.value, pleo.location.value), 177 | pleo.location) 178 | except RegexFailedToMatch, rftm: 179 | raise ParseException('Invalid %s: "%s"' % (cls.description, region.value), region) 180 | parsed_instance = cls.parse_from_matched_region(region) 181 | parsed_instance.source = region 182 | return parsed_instance 183 | 184 | class Cut(ParseableFromRegex): 185 | description = 'cut' 186 | parse_regex = regex.compile(r'[!]') 187 | 188 | @classmethod 189 | def parse_from_matched_region(cls, region): 190 | return Cut() 191 | 192 | def unparse(self): 193 | return '!' 194 | 195 | class Tie(ParseableFromRegex): 196 | description = 'tie' 197 | parse_regex = regex.compile(r'[~]') 198 | 199 | cuttable = True 200 | 201 | @classmethod 202 | def parse_from_matched_region(cls, region): 203 | return Tie() 204 | 205 | def unparse(self): 206 | return '~' 207 | 208 | def as_data(self): 209 | return {} 210 | 211 | def resolve(self, song): 212 | if song.awaiting_tie: 213 | raise ParseException('Tie appears after previous tie', self.source) 214 | if song.last_note: 215 | if song.last_note.to_continue: 216 | raise ParseException('Tie appears, but last note already marked to continue', self.source) 217 | song.last_note.to_continue = True 218 | else: 219 | raise ParseException('Tie appears, but there is no previous note', self.source) 220 | song.awaiting_tie = True 221 | 222 | class BarLine(ParseableFromRegex): 223 | 224 | description = 'bar line' 225 | parse_regex = regex.compile(r'[|]') 226 | 227 | cuttable = True 228 | 229 | def resolve(self, song): 230 | song.playing = True 231 | if song.last_bar_tick is None: 232 | part_bar_ticks = song.tick 233 | if part_bar_ticks > song.ticks_per_bar: 234 | raise ParseException('First partial bar is %s ticks long > %s ticks per bar' * 235 | (part_bar_ticks, song.ticks_per_bar), self.source) 236 | else: 237 | bar_ticks = song.tick - song.last_bar_tick 238 | if bar_ticks != song.ticks_per_bar: 239 | raise ParseException("Completed bar is %s ticks long, but expected %s ticks" % 240 | (bar_ticks, song.ticks_per_bar), self.source) 241 | song.record_bar_tick(song.tick) 242 | song.current_duration = DEFAULT_DURATION 243 | 244 | @classmethod 245 | def parse_from_matched_region(cls, region): 246 | return BarLine() 247 | 248 | def unparse(self): 249 | return '|' 250 | 251 | def as_data(self): 252 | return {} 253 | 254 | def scale_note(string): 255 | return ScaleNote.parse_with_octave(string) 256 | 257 | class ScaleNote(ParseableFromRegex): 258 | def __init__(self, note, sharps = 0, upper_case = True, octave = None): 259 | self.note = note 260 | self.sharps = sharps 261 | self.upper_case = upper_case 262 | self.semitone_offset = DIATONIC_OFFSETS[self.note] + self.sharps 263 | if octave: 264 | self.set_octave(octave) 265 | else: 266 | self.octave = None 267 | 268 | def set_sharps(self, sharps = 0): 269 | self.sharps = sharps 270 | self.semitone_offset = DIATONIC_OFFSETS[self.note] + self.sharps 271 | 272 | parse_with_octave_regex = regex.compile('^(?P[a-gA-G])(?P[+-]?)(?P[0-9])?$') 273 | 274 | @classmethod 275 | def parse_with_octave(self, string): 276 | match = self.parse_with_octave_regex.match(string) 277 | if match: 278 | groupdict = match.groupdict() 279 | sharps_string = groupdict['sharp_or_flat'] 280 | if sharps_string == '+': 281 | sharps = 1 282 | elif sharps_string == '-': 283 | sharps = -1 284 | else: 285 | sharps = 0 286 | letter = groupdict['letter'] 287 | scale_note = ScaleNote(scale_number_from_letter(letter), 288 | sharps, upper_case = letter[0].isupper()) 289 | octave_string = groupdict['octave'] 290 | if octave_string: 291 | scale_note.set_octave(int(octave_string)) 292 | return scale_note 293 | else: 294 | raise ParseException('Invalid scale note with octave: %r' % string) 295 | 296 | def set_octave(self, octave): 297 | self.octave = octave 298 | self.midi_note = valid_midi_note(12 + self.octave*12 + self.semitone_offset) 299 | 300 | def set_upward_from(self, note): 301 | semitones_up = (144 + self.semitone_offset - note.midi_note) % 12 302 | self.midi_note = valid_midi_note(note.midi_note + semitones_up) 303 | 304 | def as_data(self): 305 | return dict(note=self.note, sharps=self.sharps, upper_case=self.upper_case) 306 | 307 | def unparse(self): 308 | letter_string = (NOTE_NAMES_UPPER_CASE if self.upper_case else NOTE_NAMES_LOWER_CASE)[self.note] 309 | sharps_string = "+" * self.sharps if self.sharps > 0 else "-" * -self.sharps 310 | return letter_string + sharps_string 311 | 312 | class RelativeScale(Parseable): 313 | 314 | named_scale_positions = dict(major = [0, 2, 4, 5, 7, 9, 11], 315 | minor = [0, 2, 3, 5, 7, 8, 10]) 316 | 317 | @classmethod 318 | def get_named_scale(cls, name): 319 | return RelativeScale(name, cls.named_scale_positions[name]) 320 | 321 | def __init__(self, name, positions): 322 | self.name = name 323 | self.positions = positions 324 | self.num_positions = len(positions) 325 | self.position_lookup = dict([(position, i) for i, position in enumerate(positions)]) 326 | 327 | def as_data(self): 328 | return (self.name, self.positions) 329 | 330 | def get_position(self, semitone_position): 331 | octave_semitone_position = semitone_position % 12 332 | scale_position = self.position_lookup.get(octave_semitone_position) 333 | if scale_position is None: 334 | return None 335 | else: 336 | return self.num_positions * ((semitone_position - octave_semitone_position)/12) + scale_position 337 | 338 | class Scale(ParseableFromRegex): 339 | 340 | description = 'scale' 341 | 342 | parse_regex = regex.compile(r'(?P\S+)\s+(?Pmajor|minor)') 343 | 344 | def __init__(self, scale_note, relative_scale): 345 | self.scale_note = scale_note 346 | self.relative_scale = relative_scale 347 | 348 | def as_data(self): 349 | return (self.scale_note, self.relative_scale) 350 | 351 | @classmethod 352 | def parse_from_matched_region(cls, region): 353 | scale_note = ScaleNote.parse_with_octave(region.named_group('root_note').value) 354 | scale_name = region.named_group('scale_name').value 355 | return Scale(scale_note, RelativeScale.get_named_scale(scale_name)) 356 | 357 | def get_position(self, midi_note): 358 | return self.relative_scale.get_position(midi_note - self.scale_note.midi_note) 359 | 360 | class Chord(ParseableFromRegex): 361 | 362 | cuttable = True 363 | 364 | def __init__(self, root_note, other_notes = None, descriptor = None, bass_note = None): 365 | self.root_note = root_note 366 | self.other_notes = other_notes 367 | self.descriptor = descriptor 368 | self.bass_note = bass_note 369 | 370 | def get_midi_notes(self): 371 | if self.descriptor is not None: 372 | offsets = CHORD_DESCRIPTOR_OFFSETS[self.descriptor] 373 | return [valid_midi_note(self.root_note.midi_note + offset) for offset in offsets] 374 | else: 375 | return [valid_midi_note(note.midi_note) for note in [self.root_note] + self.other_notes] 376 | 377 | def get_bass_midi_note(self, bass_octave): 378 | bass_note = self.bass_note or self.root_note 379 | return valid_midi_note(12 + bass_octave*12 + bass_note.semitone_offset) if bass_note else None 380 | 381 | CHORD_NOTES_MATCHER = r'(:(?P([A-G][+-]?)+))' 382 | ROOT_MATCHER = r'(?P[A-G]([+-]?))' 383 | DESCRIPTOR_MATCHER = r'(?P7|m|m7|maj7|)' 384 | BASS_MATCHER = r'(/(?P[A-G][+-]?))' 385 | 386 | CHORD_MATCHER = r'\[(((%s|%s%s)%s?)|)\]' % (CHORD_NOTES_MATCHER, ROOT_MATCHER, DESCRIPTOR_MATCHER, BASS_MATCHER) 387 | parse_regex = regex.compile(CHORD_MATCHER) 388 | 389 | def resolve(self, song): 390 | song.playing = True 391 | if song.last_chord: 392 | song.last_chord.finish(song) 393 | self.tick = song.tick 394 | chord_track = song.tracks['chord'] 395 | chord_octave = chord_track.octave 396 | if self.root_note is None: 397 | self.midi_notes = [] 398 | else: 399 | self.root_note.set_octave(chord_octave) 400 | if self.other_notes: 401 | last_note = self.root_note 402 | for note in self.other_notes: 403 | note.set_upward_from(last_note) 404 | last_note = note 405 | self.midi_notes = self.get_midi_notes() 406 | song.last_chord = self 407 | chord_track.add(self) 408 | bass_track = song.tracks['bass'] 409 | bass_octave = bass_track.octave 410 | self.bass_midi_note = self.get_bass_midi_note(bass_octave) 411 | 412 | def visit_midi_track(self, midi_track): 413 | midi_notes = self.midi_notes 414 | if midi_track.transpose != 0: 415 | with parse_source(self.source): 416 | midi_notes = [valid_midi_note(midi_note+midi_track.transpose) for midi_note in midi_notes] 417 | midi_track.add_notes(midi_notes, self.tick, self.duration_ticks) 418 | 419 | def finish(self, song): 420 | self.duration_ticks = song.tick - self.tick 421 | if self.bass_midi_note: 422 | bass_track_note = BassNote(self.bass_midi_note, self.tick, self.duration_ticks, self.source) 423 | bass_track = song.tracks['bass'] 424 | bass_track.add(bass_track_note) 425 | 426 | def as_data(self): 427 | return dict(root_note=self.root_note, other_notes=self.other_notes, 428 | descriptor=self.descriptor, bass_note = self.bass_note) 429 | 430 | def unparse(self): 431 | if self.descriptor: 432 | return "[%s%s]" % (self.root_note.unparse(), self.descriptor) 433 | else: 434 | return "[:%s%s]" % (self.root_note.unparse(), "".join([note.unparse() for note in self.other_notes])) 435 | 436 | description = 'chord' 437 | 438 | @classmethod 439 | def get_chord_notes_from_string(cls, chord_notes_string): 440 | notes = [] 441 | for char in chord_notes_string: 442 | if char in ['-', '+']: 443 | notes[-1].set_sharps(1 if char == '+' else -1) 444 | else: 445 | notes.append(ScaleNote(scale_number_from_letter(char))) 446 | return notes 447 | 448 | @classmethod 449 | def parse_one_note(cls, note_string): 450 | if note_string: 451 | return cls.get_chord_notes_from_string(note_string)[0] 452 | else: 453 | return None 454 | 455 | @classmethod 456 | def parse_from_matched_region(cls, region): 457 | group_dict = region.match_groupdict 458 | chord_notes_string = group_dict['chord_notes'] 459 | bass_note_string = group_dict['bass'] 460 | bass_note = cls.parse_one_note(bass_note_string) 461 | if chord_notes_string: 462 | notes = cls.get_chord_notes_from_string(chord_notes_string) 463 | return Chord(notes[0], other_notes = notes[1:], bass_note = bass_note) 464 | else: 465 | root_note_string = group_dict['chord_root'] 466 | root_note = cls.parse_one_note(root_note_string) 467 | descriptor = group_dict['chord_descriptor'] 468 | return Chord(root_note, descriptor = descriptor, bass_note = bass_note) 469 | 470 | 471 | def resolve_duration(note_or_rest, song): 472 | x, y = note_or_rest.duration 473 | if song.ticks_per_crotchet % y != 0: 474 | raise ParseException('Duration %d/%d is not compatible with ticks per crotchet of %d' % 475 | (x, y, song.ticks_per_crotchet), note_or_rest.source) 476 | note_or_rest.duration_ticks = x * (song.ticks_per_crotchet/y) 477 | song.tick += note_or_rest.duration_ticks 478 | 479 | class Rest(ParseableFromRegex): 480 | def __init__(self, duration): 481 | self.duration = duration 482 | self.cuttable = True 483 | 484 | def as_data(self): 485 | return self.duration 486 | 487 | def resolve(self, song): 488 | song.playing = True 489 | self.tick = song.tick 490 | resolve_duration(self, song) 491 | 492 | def find_next_note(last_note, offset, ups, location = None): 493 | last_note_offset = last_note % 12 494 | if last_note_offset == offset: 495 | return last_note + 12 * ups 496 | else: 497 | last_note_from_just_below = last_note_offset if last_note_offset < offset else last_note_offset-12 498 | jump_up_from_below = offset - last_note_from_just_below 499 | this_note_above_last_note = last_note + jump_up_from_below 500 | if ups == 0: 501 | if jump_up_from_below < 6: 502 | return this_note_above_last_note 503 | elif jump_up_from_below > 6: 504 | return this_note_above_last_note - 12 505 | else: 506 | raise ParseException("Can't decide next nearest note (6 semitones either way)", location) 507 | elif ups > 0: 508 | return this_note_above_last_note + (ups-1) * 12 509 | elif ups < 0: 510 | return this_note_above_last_note + ups*12 511 | 512 | 513 | class BassNote(object): 514 | 515 | def __init__(self, midi_note, tick, duration_ticks, source): 516 | self.midi_note = midi_note 517 | self.tick = tick 518 | self.duration_ticks = duration_ticks 519 | self.source = source 520 | 521 | def visit_midi_track(self, midi_track): 522 | midi_note = self.midi_note 523 | if midi_track.transpose != 0: 524 | with parse_source(self.source): 525 | midi_note = valid_midi_note(midi_note + midi_track.transpose) 526 | midi_track.add_note(midi_note, self.tick, self.duration_ticks) 527 | 528 | class Note(ParseableFromRegex): 529 | 530 | cuttable = True 531 | 532 | def __init__(self, note, sharps = 0, ups = 0, duration = None, 533 | to_continue = False, continued = False): 534 | self.ups = ups 535 | self.note = note 536 | self.sharps = sharps 537 | self.duration = duration 538 | self.to_continue = to_continue 539 | self.continued = continued 540 | self.semitone_offset = DIATONIC_OFFSETS[self.note] + self.sharps 541 | self.continuation_start = self 542 | 543 | def resolve(self, song): 544 | song.playing = True 545 | self.tick = song.tick 546 | if self.duration is None: 547 | self.duration = song.current_duration 548 | melody_track = song.tracks['melody'] 549 | if song.last_note: 550 | self.resolve_from_last_note(song.last_note) 551 | else: 552 | self.octave = melody_track.octave 553 | self.midi_note = valid_midi_note(12 + self.octave*12 + self.semitone_offset) 554 | resolve_duration(self, song) 555 | song.current_duration = self.duration 556 | melody_track.add(self) 557 | self.resolve_continuation(song) 558 | song.last_note = self 559 | 560 | def resolve_continuation(self, song): 561 | if song.awaiting_tie: 562 | if self.continued: 563 | raise ParseException('Note unnecessarily marked to continue after preceding tie', 564 | self.source) 565 | self.continued = True 566 | song.awaiting_tie = False 567 | if self.continued: 568 | if song.last_note: 569 | if not song.last_note.to_continue: 570 | raise ParseException('Note marked as continued, but previous note not marked as to continue', 571 | self.source) 572 | if self.midi_note != song.last_note.midi_note: 573 | raise ParseException('Continued note is not the same pitch as previous note', self.source) 574 | self.continuation_start = song.last_note.continuation_start 575 | self.continuation_start.duration_ticks += self.duration_ticks 576 | else: 577 | raise ParseException('Note marked as continued, but there is no previous note', self.source) 578 | else: 579 | if song.last_note and song.last_note.to_continue: 580 | raise ParseException('Note not marked as continued, but previous note was marked as to continue', 581 | self.source) 582 | 583 | 584 | def visit_midi_track(self, midi_track): 585 | if not self.continued: 586 | midi_note = self.midi_note 587 | if midi_track.transpose != 0: 588 | with parse_source(self.source): 589 | midi_note = valid_midi_note(midi_note + midi_track.transpose) 590 | midi_track.add_note(midi_note, self.tick, self.duration_ticks) 591 | 592 | def resolve_from_last_note(self, last_note): 593 | self.midi_note = valid_midi_note(find_next_note(last_note.midi_note, self.semitone_offset, self.ups, location = self.source)) 594 | 595 | CONTINUED_MATCHER = r'(?P[~])?' 596 | UPS_OR_DOWNS_MATCHER = r'((?P\'+)|(?P,+)|)' 597 | NOTE_NAME_MATCHER = r'(?P[a-g][+-]?)' 598 | REST_MATCHER = r'(?Pr)' 599 | DURATION_MATCHER = r'(?P[1-9][0-9]*)?(?P[hq]+)?(?Pt)?(?P[.])?' 600 | TO_CONTINUE_MATCHER = r'(?P[~])?' 601 | NOTE_COMMAND_MATCHER = '%s(%s|%s%s)%s%s' % (CONTINUED_MATCHER, REST_MATCHER, 602 | NOTE_NAME_MATCHER, UPS_OR_DOWNS_MATCHER, DURATION_MATCHER, TO_CONTINUE_MATCHER) 603 | parse_regex = regex.compile(NOTE_COMMAND_MATCHER) 604 | 605 | def as_data(self): 606 | return dict(ups = self.ups, note = self.note, 607 | sharps = self.sharps, duration = self.duration, 608 | continued = self.continued, to_continue = self.to_continue) 609 | 610 | description = 'note' 611 | 612 | @classmethod 613 | def parse_from_matched_region(cls, region): 614 | group_dict = region.match_groupdict 615 | ups = len(group_dict['ups'] or '') 616 | downs = len(group_dict['downs'] or '') 617 | ups = ups - downs 618 | if group_dict['rest']: 619 | note = None 620 | else: 621 | note_string = group_dict['note'] 622 | note = scale_number_from_letter(note_string[0]) 623 | if len(note_string) == 2: 624 | if note_string[1] == '+': 625 | sharps = 1 626 | else: 627 | sharps = -1 628 | else: 629 | sharps = 0 630 | duration_found = False 631 | x = 1 632 | y = 1 633 | if group_dict['beats']: 634 | x = x*int(group_dict['beats']) 635 | duration_found = True 636 | if group_dict['hqs']: 637 | for ch in group_dict['hqs']: 638 | if ch == 'h': 639 | y = y*2 640 | if ch == 'q': 641 | y = y*4 642 | duration_found = True 643 | if group_dict['triplet']: 644 | y = y*3 645 | duration_found = True 646 | if group_dict['dot']: 647 | x = x*3 648 | y = y*2 649 | duration_found = True 650 | if duration_found: 651 | duration = (x, y) 652 | else: 653 | duration = None 654 | to_continue = group_dict['to_continue'] == '~' 655 | continued = group_dict['continued'] == '~' 656 | if note is None: 657 | if duration is None: 658 | raise ParseException('Rest must specify duration', region) 659 | return Rest(duration) 660 | else: 661 | return Note(note, sharps, ups, duration, to_continue = to_continue, continued = continued) 662 | 663 | def unparse(self): 664 | ups_string = "'" * self.ups if self.ups > 0 else "," * -self.ups 665 | letter_string = "r" if self.note is None else NOTE_NAMES_LOWER_CASE[self.note] 666 | sharps_string = "+" * self.sharps if self.sharps > 0 else "-" * -self.sharps 667 | x, y = self.duration 668 | duration_string = "" if x == 1 else str(x) 669 | while y > 1: 670 | old_y = y 671 | 672 | if y%4 == 0: 673 | duration_string += "q" 674 | y = y/4 675 | elif y%3 == 0: 676 | duration_string += "t" 677 | y = y/3 678 | elif y%2 == 0: 679 | duration_string += "h" 680 | y = y/2 681 | if y == old_y and y > 1: 682 | raise Exception("Can't notate duration quotient %s" % y) 683 | return letter_string + sharps_string + ups_string + duration_string 684 | 685 | class ParseFromChoices(object): 686 | 687 | @classmethod 688 | def parse(self, region): 689 | match = region.match(self.choice_regex) 690 | if match: 691 | group_dict = match.groupdict() 692 | group_dict_items = [(k,v) for k, v in group_dict.items() if v is not None] 693 | if len(group_dict_items) == 1: 694 | key, value = group_dict_items[0] 695 | return self.class_dict[key].parse(region) 696 | elif len(group_dict) == 0: 697 | raise Exception('Parse regex matched, but didn\'t match any items in class dict') 698 | else: 699 | raise Exception('Ambiguous regex, parse result = %r' % group_dict) 700 | else: 701 | raise ParseException('Invalid %s: %r' % (self.description, region.value), region) 702 | 703 | 704 | class SongItem(ParseFromChoices): 705 | 706 | description = 'song item' 707 | 708 | choice_regex = regex.compile(r'(?P[[])|(?P[|])|(?P[~]?[Va-gr^])|(?P[~])|(?P[!])') 709 | 710 | class_dict = dict(chord = Chord, barline = BarLine, note = Note, cut = Cut, tie = Tie) 711 | 712 | 713 | class ParseItems(ParseableFromRegex): 714 | 715 | @classmethod 716 | def parse_from_matched_region(cls, region): 717 | item_regions = region.named_groups('item') 718 | return cls([cls.item_class.parse(item_region) for item_region in item_regions]) 719 | 720 | def __init__(self, items): 721 | self.items = items 722 | 723 | def __iter__(self): 724 | return iter(self.items) 725 | 726 | def as_data(self): 727 | return self.items 728 | 729 | 730 | class SongItems(ParseItems): 731 | 732 | parse_regex = regex.compile(r'\s*((?P\S+)\s*)*') 733 | 734 | item_class = SongItem 735 | 736 | 737 | class IntValueParser(object): 738 | 739 | def __init__(self, label, min_value, max_value): 740 | self.label = label 741 | self.min_value = min_value 742 | self.max_value = max_value 743 | 744 | def raise_invalid_value_exception(self, value, value_region): 745 | raise ParseException('Invalid value for %s: %r - must be an integer from %s to %s' % 746 | (self.label, value, self.min_value, self.max_value), 747 | value_region) 748 | 749 | def parse(self, value_region): 750 | value_string = value_region.value 751 | try: 752 | value = int(value_string) 753 | except ValueError: 754 | self.raise_invalid_value_exception(value_string, value_region) 755 | if value < self.min_value or value > self.max_value: 756 | self.raise_invalid_value_exception(value, value_region) 757 | return value 758 | 759 | class TimeSignatureParser(object): 760 | 761 | parse_regex = regex.compile(r'(?P[1-9][0-9]*)\s*[/](?P2|4|8|16|32)') 762 | 763 | def parse(self, value_region): 764 | value_region.parse(self.parse_regex) 765 | numerator = int(value_region.match_groupdict['numerator']) 766 | denominator = int(value_region.match_groupdict['denominator']) 767 | return numerator, denominator 768 | 769 | class ValueSetter(Parseable): 770 | 771 | def __init__(self, value): 772 | self.value = value 773 | 774 | def as_data(self): 775 | return self.value 776 | 777 | @classmethod 778 | def parse_value(cls, value_region): 779 | value = cls.value_parser.parse(value_region) 780 | value_setter = cls(value) 781 | return value_setter 782 | 783 | class RawValueSetting(ParseableFromRegex): 784 | 785 | parse_regex = regex.compile(r'((?P[^=\s]+)\s*=\s*(?P.*))') 786 | 787 | description = 'value setting' 788 | 789 | @classmethod 790 | def parse_from_matched_region(cls, region): 791 | return RawValueSetting(region.named_group('key'), region.named_group('value')) 792 | 793 | def __init__(self, key_region, value_region): 794 | self.key_region = key_region 795 | self.value_region = value_region 796 | 797 | class Command(ParseableFromRegex): 798 | 799 | @classmethod 800 | def require_no_qualifier(cls, qualifier_region): 801 | if qualifier_region is not None: 802 | raise ParseException('Unexpected qualifier in %r command' % cls.description, 803 | qualifier_region) 804 | 805 | class ValuesCommand(Command): 806 | 807 | items_regex = regex.compile(r'((?P[^,]+)([,]\s*(?P[^\s,][^,]*))*)') 808 | 809 | def __init__(self, values): 810 | self.values = values 811 | self.cuttable = False 812 | 813 | def as_data(self): 814 | return self.values 815 | 816 | @classmethod 817 | def parse_value_setting(cls, region): 818 | 819 | raw_value_setting = RawValueSetting.parse(region) 820 | key = raw_value_setting.key_region.value 821 | value_setter_class = cls.value_setters.get(key) 822 | if value_setter_class is None: 823 | raise ParseException('Invalid value key for %s: %r' % (cls.description, key), 824 | raw_value_setting.key_region) 825 | value_setting = value_setter_class.parse_value(raw_value_setting.value_region) 826 | value_setting.source = region 827 | return value_setting 828 | 829 | 830 | class SetSongTempoBpm(ValueSetter): 831 | key = 'tempo_bpm' 832 | value_parser = IntValueParser('tempo_bpm', 1, 1000) 833 | 834 | def resolve(self, song): 835 | song.unplayed().tempo_bpm = self.value 836 | 837 | class SetSongTimeSignature(ValueSetter): 838 | key = 'time_signature', 839 | value_parser = TimeSignatureParser() 840 | 841 | def resolve(self, song): 842 | song.unplayed().set_time_signature(self.value) 843 | 844 | class SetSongScale(ValueSetter): 845 | value_parser = Scale 846 | 847 | def resolve(self, song): 848 | if self.value.scale_note.octave is None: 849 | self.value.scale_note.set_octave(song.tracks['melody'].octave) 850 | song.unplayed().scale = self.value 851 | 852 | class SetSongTicksPerBeat(ValueSetter): 853 | value_parser = IntValueParser('ticks_per_beat', 1, 2000) 854 | 855 | def resolve(self, song): 856 | song.unplayed().set_ticks_per_beat(self.value) 857 | 858 | class SetSongSubTicksPerTick(ValueSetter): 859 | value_parser = IntValueParser('subticks_per_tick', 1, 100) 860 | 861 | def resolve(self, song): 862 | song.unplayed().set_subticks_per_tick(self.value) 863 | 864 | class SetSongTranspose(ValueSetter): 865 | value_parser = IntValueParser('transpose', -127, 127) 866 | 867 | def resolve(self, song): 868 | song.unplayed().transpose = self.value 869 | 870 | class SongValuesCommand(ValuesCommand): 871 | 872 | description = 'song' 873 | 874 | value_setters = dict(tempo_bpm = SetSongTempoBpm, 875 | time_signature = SetSongTimeSignature, 876 | ticks_per_beat = SetSongTicksPerBeat, 877 | subticks_per_tick = SetSongSubTicksPerTick, 878 | transpose = SetSongTranspose, 879 | scale = SetSongScale) 880 | 881 | @classmethod 882 | def parse_command(cls, qualifier_region, body_region): 883 | cls.require_no_qualifier(qualifier_region) 884 | body_region.parse(cls.items_regex) 885 | item_regions = body_region.named_groups('item') 886 | return SongValuesCommand(values = [cls.parse_value_setting(item_region.stripped()) 887 | for item_region in item_regions]) 888 | 889 | @classmethod 890 | def get_init_args(cls, values, match): 891 | return [values] 892 | 893 | def resolve(self, song): 894 | for value in self.values: 895 | with parse_source(value.source): 896 | value.resolve(song) 897 | 898 | 899 | class SetTrackInstrument(ValueSetter): 900 | value_parser = IntValueParser('instrument', 0, 127) 901 | 902 | def resolve(self, track): 903 | track.unplayed().instrument = self.value 904 | 905 | class SetTrackVolume(ValueSetter): 906 | value_parser = IntValueParser('volume', 0, 127) 907 | 908 | def resolve(self, track): 909 | track.unplayed().volume = self.value 910 | 911 | class SetTrackOctave(ValueSetter): 912 | value_parser = IntValueParser('octave', -1, 10) 913 | 914 | def resolve(self, track): 915 | track.unplayed().octave = self.value 916 | 917 | class TrackValuesCommand(ValuesCommand): 918 | 919 | description = 'track' 920 | 921 | def __init__(self, name, values): 922 | super(TrackValuesCommand, self).__init__(values) 923 | self.name = name 924 | 925 | def as_data(self): 926 | return (self.name, self.values) 927 | 928 | value_setters = dict(instrument = SetTrackInstrument, 929 | volume = SetTrackVolume, 930 | octave = SetTrackOctave) 931 | 932 | @classmethod 933 | def parse_command(cls, qualifier_region, body_region): 934 | if qualifier_region is None: 935 | raise ParseException('Missing qualifier in %r command' % cls.description, 936 | qualifier_region) 937 | body_region.parse(cls.items_regex) 938 | item_regions = body_region.named_groups('item') 939 | return TrackValuesCommand(qualifier_region.value, 940 | values = [cls.parse_value_setting(item_region.stripped()) 941 | for item_region in item_regions]) 942 | 943 | @classmethod 944 | def get_init_args(cls, values, match): 945 | name = match.groupdict()['name'] 946 | return [name, values] 947 | 948 | def resolve(self, song): 949 | track = song.tracks.get(self.name) 950 | if track is None: 951 | raise ParseException('Unknown track: %r' % self.name) 952 | for value in self.values: 953 | with parse_source(value.source): 954 | value.resolve(track) 955 | 956 | class GrooveDelay(ParseableFromRegex): 957 | 958 | description = 'groove delay' 959 | 960 | parse_regex = regex.compile(r'(?P[-+]?[0-9]+)') 961 | 962 | def __init__(self, delay): 963 | self.delay = delay 964 | 965 | def as_data(self): 966 | return self.delay 967 | 968 | @classmethod 969 | def parse_from_matched_region(cls, region): 970 | return GrooveDelay(int(region.match_groupdict['delay'])) 971 | 972 | class GrooveDelays(ParseItems): 973 | 974 | description = 'groove delays' 975 | 976 | parse_regex = regex.compile(r'\s*((?P\S+)\s*)+') 977 | item_class = GrooveDelay 978 | 979 | 980 | class GrooveCommand(Command): 981 | 982 | description = 'groove command' 983 | 984 | delays_regex = regex.compile(r'((?P[-+]?[0-9]+)\s*)+') 985 | 986 | def __init__(self, delays): 987 | self.delays = delays 988 | 989 | def as_data(self): 990 | return self.delays 991 | 992 | @classmethod 993 | def parse_delay(cls, region): 994 | try: 995 | delay = int(region.value) 996 | return delay 997 | except ValueError: 998 | raise ParseException('Invalid groove delay: %r' % region.value, region) 999 | 1000 | def resolve(self, song): 1001 | song.groove_delays = self.delays 1002 | song.set_groove() 1003 | 1004 | @classmethod 1005 | def parse_command(cls, qualifier_region, body_region): 1006 | cls.require_no_qualifier(qualifier_region) 1007 | groove_delays = GrooveDelays.parse(body_region) 1008 | return GrooveCommand(delays = [groove_delay.delay for groove_delay in groove_delays]) 1009 | 1010 | class NamedCommand(ParseableFromRegex): 1011 | parse_regex = regex.compile(r'(?P[^.:]+)(.(?P[^:]+))?:\s*(?P.*)') 1012 | 1013 | @classmethod 1014 | def parse_from_matched_region(cls, region): 1015 | command = region.match_groupdict['command'] 1016 | command_class = cls.commands.get(command) 1017 | if command_class is None: 1018 | raise ParseException('Unknown %s: %r' % (cls.description, command), 1019 | region) 1020 | return command_class.parse_command(region.named_group('qualifier'), 1021 | region.named_group('body')) 1022 | 1023 | class SongCommand(NamedCommand): 1024 | 1025 | description = 'song command' 1026 | 1027 | commands = dict(song = SongValuesCommand, 1028 | groove = GrooveCommand, 1029 | track = TrackValuesCommand) 1030 | 1031 | class Groove(ParseableFromRegex): 1032 | 1033 | def __init__(self, beats_per_bar, ticks_per_beat, subticks_per_tick, delays): 1034 | self.beats_per_bar = beats_per_bar 1035 | self.ticks_per_beat = ticks_per_beat 1036 | self.subticks_per_tick = subticks_per_tick 1037 | self.delays = delays 1038 | self.num_delays = len(self.delays) 1039 | self.ticks_per_bar = beats_per_bar * ticks_per_beat 1040 | if self.ticks_per_bar % self.num_delays != 0: 1041 | raise ParseException('%d delays (%r) given for groove, but %d ticks per bar is not a multiple of %d' % 1042 | (self.num_delays, self.delays, self.ticks_per_bar, self.num_delays)) 1043 | 1044 | def as_data(self): 1045 | return dict(beats_per_bar = self.beats_per_bar, 1046 | ticks_per_beat = self.ticks_per_beat, 1047 | subticks_per_tick = self.subticks_per_tick, 1048 | delays = self.delays) 1049 | 1050 | def get_subticks(self, tick): 1051 | return tick * self.subticks_per_tick + self.delays[tick % self.num_delays] 1052 | 1053 | class Track(object): 1054 | 1055 | def __init__(self, song, octave = 3): 1056 | self.song = song 1057 | self.octave = octave 1058 | self.volume = 100 1059 | self.instrument = 0 1060 | self.items = [] 1061 | 1062 | def clear_items(self): 1063 | self.items = [] 1064 | 1065 | def unplayed(self): 1066 | self.song.unplayed() 1067 | return self 1068 | 1069 | def add(self, item): 1070 | self.items.append(item) 1071 | 1072 | class Song(Parseable): 1073 | 1074 | LINE_REGEX = regex.compile('\s*([*](?P.*)|(?P.*))') 1075 | 1076 | def __init__(self, items = None): 1077 | self.items = [] if items is None else items[:] 1078 | self.transpose = 0 1079 | self.time_signature = (4, 4) 1080 | self.ticks_per_beat = 4 1081 | self.subticks_per_tick = 1 1082 | self.groove_delays = [0] 1083 | self.recalculate_tick_values() 1084 | self.playing = False # some command can only happen before it starts playing 1085 | self.tempo_bpm = 120 1086 | self.set_to_start() 1087 | self.tracks = dict(melody = Track(self, octave = 3), chord = Track(self, octave = 1), 1088 | bass = Track(self, octave = 0)) 1089 | 1090 | @property 1091 | def beats_per_bar(self): 1092 | return self.time_signature[0] 1093 | 1094 | def set_groove(self): 1095 | self.groove = Groove(self.beats_per_bar, self.ticks_per_beat, self.subticks_per_tick, 1096 | self.groove_delays) 1097 | 1098 | def record_bar_tick(self, tick): 1099 | if self.last_bar_tick is None: 1100 | if tick == 0: 1101 | self.bar_ticks = [tick] 1102 | else: 1103 | first_tick = tick - self.ticks_per_bar 1104 | self.bar_ticks = [first_tick, tick] 1105 | else: 1106 | self.bar_ticks.append(tick) 1107 | self.last_bar_tick = tick 1108 | 1109 | def clear_items(self): 1110 | self.items = [item for item in self.items if not item.cuttable] 1111 | self.set_to_start() 1112 | for track in self.tracks.values(): 1113 | track.clear_items() 1114 | 1115 | def set_to_start(self): 1116 | self.last_bar_tick = None 1117 | self.tick = 0 # current time in ticks 1118 | self.last_note = None 1119 | self.current_duration = DEFAULT_DURATION 1120 | self.last_chord = None 1121 | self.last_bar_tick = None 1122 | self.awaiting_tie = False 1123 | 1124 | @property 1125 | def subticks_per_second(self): 1126 | subticks_per_minute = self.tempo_bpm * self.ticks_per_beat * self.subticks_per_tick 1127 | return subticks_per_minute/60.0 1128 | 1129 | def set_ticks_per_beat(self, ticks_per_beat): 1130 | self.ticks_per_beat = ticks_per_beat 1131 | self.recalculate_tick_values() 1132 | 1133 | def set_subticks_per_tick(self, subticks_per_tick): 1134 | self.subticks_per_tick = subticks_per_tick 1135 | self.recalculate_tick_values() 1136 | 1137 | def set_groove_delays(self, delays): 1138 | self.groove_delays = delays 1139 | self.set_groove() 1140 | 1141 | def recalculate_tick_values(self): 1142 | self.ticks_per_bar = self.beats_per_bar * self.ticks_per_beat 1143 | if self.time_signature[1] >= 4: 1144 | beats_per_crotchet = self.time_signature[1]/4 1145 | self.ticks_per_crotchet = self.ticks_per_beat * beats_per_crotchet 1146 | else: 1147 | crotchets_per_beat = 4/self.time_signature[1] 1148 | if self.ticks_per_beat % crotchets_per_beat != 0: 1149 | raise ParseException('ticks per beat of %d has to be a multiple of crotchets per beat of %d' % 1150 | (self.ticks_per_beat, crotchets_per_beat)) 1151 | self.ticks_per_crotchet = self.ticks_per_beat / crotchets_per_beat 1152 | self.set_groove() 1153 | 1154 | def set_time_signature(self, time_signature): 1155 | self.time_signature = time_signature 1156 | self.recalculate_tick_values() 1157 | 1158 | def unplayed(self): 1159 | if self.playing: 1160 | raise ParseException('Cannot perform operation once song is playing') 1161 | return self 1162 | 1163 | def as_data(self): 1164 | return self.items 1165 | 1166 | def add(self, item): 1167 | if item.__class__ == Cut: 1168 | self.clear_items() 1169 | else: 1170 | with parse_source(item.source): 1171 | item.resolve(self) 1172 | self.items.append(item) 1173 | 1174 | def check_last_part_bar(self): 1175 | part_bar_ticks = self.tick if self.last_bar_tick is None else self.tick - self.last_bar_tick 1176 | if part_bar_ticks > self.ticks_per_bar: 1177 | raise ParseException('Last part bar has %s ticks > %s ticks per bar' % (part_bar_ticks, self.ticks_per_bar)) 1178 | if self.last_bar_tick is not None: 1179 | self.bar_ticks.append(self.last_bar_tick + self.ticks_per_bar) # so self.bar_ticks includes all bars including part-bars 1180 | 1181 | def finish(self): 1182 | self.check_last_part_bar() 1183 | if self.last_chord: 1184 | self.last_chord.finish(self) 1185 | 1186 | @classmethod 1187 | def parse(cls, parse_file): 1188 | song = Song() 1189 | for line in parse_file.read_lines(): 1190 | for item in cls.parse_line(line): 1191 | song.add(item) 1192 | song.finish() 1193 | return song 1194 | 1195 | @classmethod 1196 | def parse_line(cls, line): 1197 | line.parse(cls.LINE_REGEX) 1198 | command_line = line.named_group('command') 1199 | if command_line: 1200 | return [SongCommand.parse(command_line)] 1201 | else: 1202 | song_items = line.named_group('song_items') 1203 | if song_items: 1204 | return SongItems.parse(song_items) 1205 | else: 1206 | raise Exception('No match for line: %r' % line.value) 1207 | 1208 | 1209 | def __repr__(self): 1210 | return 'Song[%s]' % (", ".join(["%r" % item for item in self.items])) 1211 | 1212 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | regex==2015.09.15 2 | nose==1.3.7 3 | #midi==0.2.3 4 | https://github.com/vishnubob/python-midi/archive/v0.2.3.zip 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Always prefer setuptools over distutils 2 | from setuptools import setup, find_packages 3 | # To use a consistent encoding 4 | from codecs import open 5 | from os import path 6 | 7 | here = path.abspath(path.dirname(__file__)) 8 | 9 | # Get the long description from the relevant file 10 | with open(path.join(here, 'DESCRIPTION.rst'), encoding='utf-8') as f: 11 | long_description = f.read() 12 | 13 | setup( 14 | name='melody_scripter', 15 | 16 | # Versions should comply with PEP440. For a discussion on single-sourcing 17 | # the version across setup.py and the project code, see 18 | # https://packaging.python.org/en/latest/single_source_version.html 19 | version='0.0.8', 20 | 21 | description='Melody Scripter, for parsing melodies from a simple textual format', 22 | long_description=long_description, 23 | 24 | # The project's main homepage. 25 | url='https://github.com/pdorrell/melody_scripter', 26 | 27 | # Author details 28 | author='Philip Dorrell', 29 | author_email='http://thinkinghard.com/email.html', 30 | 31 | # Choose your license 32 | license='MIT', 33 | 34 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 35 | classifiers=[ 36 | # How mature is this project? Common values are 37 | # 3 - Alpha 38 | # 4 - Beta 39 | # 5 - Production/Stable 40 | 'Development Status :: 3 - Beta', 41 | 42 | # Indicate who your project is intended for 43 | 'Intended Audience :: Developers', 44 | 'Intended Audience :: Musicians', 45 | 'Topic :: Software Development :: Music', 46 | 47 | # Pick your license as you wish (should match "license" above) 48 | 'License :: OSI Approved :: MIT License', 49 | 50 | # Specify the Python versions you support here. In particular, ensure 51 | # that you indicate whether you support Python 2, Python 3 or both. 52 | 'Programming Language :: Python :: 2', 53 | 'Programming Language :: Python :: 2.7', 54 | ], 55 | 56 | # What does your project relate to? 57 | keywords='music parsing', 58 | 59 | # You can just specify the packages manually here if your project is 60 | # simple. Or you can use find_packages(). 61 | packages=find_packages(exclude=['contrib', 'docs', 'tests*']), 62 | 63 | # List run-time dependencies here. These will be installed by pip when 64 | # your project is installed. For an analysis of "install_requires" vs pip's 65 | # requirements files see: 66 | # https://packaging.python.org/en/latest/requirements.html 67 | install_requires=['regex>=2015.09.15', 68 | 'midi>=0.2.3'], 69 | 70 | dependency_links=[ 71 | "https://github.com/vishnubob/python-midi/archive/v0.2.3.zip#egg=midi-0.2.3" 72 | ], 73 | 74 | # List additional groups of dependencies here (e.g. development 75 | # dependencies). You can install these using the following syntax, 76 | # for example: 77 | # $ pip install -e .[dev,test] 78 | extras_require={ 79 | 'dev': ['check-manifest'], 80 | 'test': ['nose'], 81 | }, 82 | 83 | # If there are data files included in your packages that need to be 84 | # installed, specify them here. If using Python 2.6 or less, then these 85 | # have to be included in MANIFEST.in as well. 86 | package_data={ 87 | }, 88 | 89 | # Although 'package_data' is the preferred approach, in some case you may 90 | # need to place data files outside of your packages. See: 91 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa 92 | # In this case, 'data_file' will be installed into '/my_data' 93 | data_files=[], 94 | 95 | # To provide executable scripts, use entry points in preference to the 96 | # "scripts" keyword. Entry points provide cross-platform support and allow 97 | # pip to create the appropriate form of executable for the target platform. 98 | entry_points={ 99 | 'console_scripts': [ 100 | 'song2midi=melody_scripter.song2midi:main', 101 | 'play_song=melody_scripter.play_song:main', 102 | ], 103 | }, 104 | ) 105 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/tests/__init__.py -------------------------------------------------------------------------------- /tests/regression/expected/amazing_grace.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/tests/regression/expected/amazing_grace.song.mid -------------------------------------------------------------------------------- /tests/regression/expected/amazing_grace.song.mid.dump: -------------------------------------------------------------------------------- 1 | midi.Pattern(format=1, resolution=4, tracks=\ 2 | [[midi.SetTempoEvent(tick=0, data=[9, 39, 192]), 3 | midi.ProgramChangeEvent(tick=0, channel=0, data=[73]), 4 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 80]), 5 | midi.NoteOffEvent(tick=2, channel=0, data=[67, 0]), 6 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 80]), 7 | midi.NoteOffEvent(tick=2, channel=0, data=[72, 0]), 8 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 80]), 9 | midi.NoteOffEvent(tick=8, channel=0, data=[72, 0]), 10 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 80]), 11 | midi.NoteOffEvent(tick=1, channel=0, data=[76, 0]), 12 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 80]), 13 | midi.NoteOffEvent(tick=1, channel=0, data=[74, 0]), 14 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 80]), 15 | midi.NoteOffEvent(tick=2, channel=0, data=[72, 0]), 16 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 80]), 17 | midi.NoteOffEvent(tick=8, channel=0, data=[76, 0]), 18 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 80]), 19 | midi.NoteOffEvent(tick=4, channel=0, data=[74, 0]), 20 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 80]), 21 | midi.NoteOffEvent(tick=8, channel=0, data=[72, 0]), 22 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 80]), 23 | midi.NoteOffEvent(tick=4, channel=0, data=[69, 0]), 24 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 80]), 25 | midi.NoteOffEvent(tick=8, channel=0, data=[67, 0]), 26 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 80]), 27 | midi.NoteOffEvent(tick=2, channel=0, data=[67, 0]), 28 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 80]), 29 | midi.NoteOffEvent(tick=2, channel=0, data=[72, 0]), 30 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 80]), 31 | midi.NoteOffEvent(tick=8, channel=0, data=[72, 0]), 32 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 80]), 33 | midi.NoteOffEvent(tick=1, channel=0, data=[76, 0]), 34 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 80]), 35 | midi.NoteOffEvent(tick=1, channel=0, data=[74, 0]), 36 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 80]), 37 | midi.NoteOffEvent(tick=2, channel=0, data=[72, 0]), 38 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 80]), 39 | midi.NoteOffEvent(tick=8, channel=0, data=[76, 0]), 40 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 80]), 41 | midi.NoteOffEvent(tick=2, channel=0, data=[74, 0]), 42 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 80]), 43 | midi.NoteOffEvent(tick=2, channel=0, data=[76, 0]), 44 | midi.NoteOnEvent(tick=0, channel=0, data=[79, 80]), 45 | midi.NoteOffEvent(tick=8, channel=0, data=[79, 0]), 46 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 80]), 47 | midi.NoteOffEvent(tick=2, channel=0, data=[76, 0]), 48 | midi.NoteOnEvent(tick=0, channel=0, data=[79, 80]), 49 | midi.NoteOffEvent(tick=2, channel=0, data=[79, 0]), 50 | midi.NoteOnEvent(tick=0, channel=0, data=[79, 80]), 51 | midi.NoteOffEvent(tick=8, channel=0, data=[79, 0]), 52 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 80]), 53 | midi.NoteOffEvent(tick=1, channel=0, data=[76, 0]), 54 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 80]), 55 | midi.NoteOffEvent(tick=1, channel=0, data=[74, 0]), 56 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 80]), 57 | midi.NoteOffEvent(tick=2, channel=0, data=[72, 0]), 58 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 80]), 59 | midi.NoteOffEvent(tick=8, channel=0, data=[76, 0]), 60 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 80]), 61 | midi.NoteOffEvent(tick=4, channel=0, data=[74, 0]), 62 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 80]), 63 | midi.NoteOffEvent(tick=8, channel=0, data=[72, 0]), 64 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 80]), 65 | midi.NoteOffEvent(tick=4, channel=0, data=[69, 0]), 66 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 80]), 67 | midi.NoteOffEvent(tick=8, channel=0, data=[67, 0]), 68 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 80]), 69 | midi.NoteOffEvent(tick=2, channel=0, data=[67, 0]), 70 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 80]), 71 | midi.NoteOffEvent(tick=2, channel=0, data=[72, 0]), 72 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 80]), 73 | midi.NoteOffEvent(tick=8, channel=0, data=[72, 0]), 74 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 80]), 75 | midi.NoteOffEvent(tick=1, channel=0, data=[76, 0]), 76 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 80]), 77 | midi.NoteOffEvent(tick=1, channel=0, data=[74, 0]), 78 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 80]), 79 | midi.NoteOffEvent(tick=2, channel=0, data=[72, 0]), 80 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 80]), 81 | midi.NoteOffEvent(tick=12, channel=0, data=[76, 0]), 82 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 80]), 83 | midi.NoteOffEvent(tick=12, channel=0, data=[74, 0]), 84 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 80]), 85 | midi.NoteOffEvent(tick=12, channel=0, data=[72, 0]), 86 | midi.EndOfTrackEvent(tick=1, data=[])], 87 | [midi.SetTempoEvent(tick=0, data=[9, 39, 192]), 88 | midi.ProgramChangeEvent(tick=0, channel=1, data=[40]), 89 | midi.NoteOnEvent(tick=4, channel=1, data=[48, 50]), 90 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 91 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 92 | midi.NoteOffEvent(tick=12, channel=1, data=[48, 0]), 93 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 94 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 95 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 96 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 97 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 98 | midi.NoteOffEvent(tick=8, channel=1, data=[48, 0]), 99 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 100 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 101 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 102 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 103 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 104 | midi.NoteOnEvent(tick=0, channel=1, data=[58, 50]), 105 | midi.NoteOffEvent(tick=4, channel=1, data=[48, 0]), 106 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 107 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 108 | midi.NoteOffEvent(tick=0, channel=1, data=[58, 0]), 109 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 110 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 111 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 112 | midi.NoteOffEvent(tick=12, channel=1, data=[53, 0]), 113 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 114 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 115 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 116 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 117 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 118 | midi.NoteOffEvent(tick=12, channel=1, data=[48, 0]), 119 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 120 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 121 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 122 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 123 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 124 | midi.NoteOffEvent(tick=24, channel=1, data=[48, 0]), 125 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 126 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 127 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 128 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 129 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 130 | midi.NoteOffEvent(tick=12, channel=1, data=[55, 0]), 131 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 132 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 133 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 134 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 135 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 136 | midi.NoteOffEvent(tick=12, channel=1, data=[55, 0]), 137 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 138 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 139 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 140 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 141 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 142 | midi.NoteOffEvent(tick=8, channel=1, data=[48, 0]), 143 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 144 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 145 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 146 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 147 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 148 | midi.NoteOnEvent(tick=0, channel=1, data=[58, 50]), 149 | midi.NoteOffEvent(tick=4, channel=1, data=[48, 0]), 150 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 151 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 152 | midi.NoteOffEvent(tick=0, channel=1, data=[58, 0]), 153 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 154 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 155 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 156 | midi.NoteOffEvent(tick=12, channel=1, data=[53, 0]), 157 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 158 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 159 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 160 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 161 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 162 | midi.NoteOffEvent(tick=12, channel=1, data=[48, 0]), 163 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 164 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 165 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 166 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 167 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 168 | midi.NoteOffEvent(tick=12, channel=1, data=[48, 0]), 169 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 170 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 171 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 172 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 173 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 174 | midi.NoteOffEvent(tick=12, channel=1, data=[48, 0]), 175 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 176 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 177 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 178 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 179 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 180 | midi.NoteOnEvent(tick=0, channel=1, data=[65, 50]), 181 | midi.NoteOffEvent(tick=12, channel=1, data=[55, 0]), 182 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 183 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 184 | midi.NoteOffEvent(tick=0, channel=1, data=[65, 0]), 185 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 186 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 187 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 188 | midi.NoteOffEvent(tick=12, channel=1, data=[48, 0]), 189 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 190 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 191 | midi.EndOfTrackEvent(tick=1, data=[])], 192 | [midi.SetTempoEvent(tick=0, data=[9, 39, 192]), 193 | midi.ProgramChangeEvent(tick=0, channel=2, data=[19]), 194 | midi.NoteOnEvent(tick=4, channel=2, data=[36, 120]), 195 | midi.NoteOffEvent(tick=12, channel=2, data=[36, 0]), 196 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 120]), 197 | midi.NoteOffEvent(tick=8, channel=2, data=[36, 0]), 198 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 120]), 199 | midi.NoteOffEvent(tick=4, channel=2, data=[36, 0]), 200 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 120]), 201 | midi.NoteOffEvent(tick=12, channel=2, data=[41, 0]), 202 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 120]), 203 | midi.NoteOffEvent(tick=12, channel=2, data=[36, 0]), 204 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 120]), 205 | midi.NoteOffEvent(tick=24, channel=2, data=[36, 0]), 206 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 120]), 207 | midi.NoteOffEvent(tick=12, channel=2, data=[43, 0]), 208 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 120]), 209 | midi.NoteOffEvent(tick=12, channel=2, data=[43, 0]), 210 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 120]), 211 | midi.NoteOffEvent(tick=8, channel=2, data=[36, 0]), 212 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 120]), 213 | midi.NoteOffEvent(tick=4, channel=2, data=[36, 0]), 214 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 120]), 215 | midi.NoteOffEvent(tick=12, channel=2, data=[41, 0]), 216 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 120]), 217 | midi.NoteOffEvent(tick=12, channel=2, data=[36, 0]), 218 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 120]), 219 | midi.NoteOffEvent(tick=12, channel=2, data=[36, 0]), 220 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 120]), 221 | midi.NoteOffEvent(tick=12, channel=2, data=[36, 0]), 222 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 120]), 223 | midi.NoteOffEvent(tick=12, channel=2, data=[43, 0]), 224 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 120]), 225 | midi.NoteOffEvent(tick=12, channel=2, data=[36, 0]), 226 | midi.EndOfTrackEvent(tick=1, data=[])]]) -------------------------------------------------------------------------------- /tests/regression/expected/canon_riff.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/tests/regression/expected/canon_riff.song.mid -------------------------------------------------------------------------------- /tests/regression/expected/canon_riff.song.mid.dump: -------------------------------------------------------------------------------- 1 | midi.Pattern(format=1, resolution=4, tracks=\ 2 | [[midi.SetTempoEvent(tick=0, data=[6, 26, 128]), 3 | midi.ProgramChangeEvent(tick=0, channel=0, data=[73]), 4 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 5 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 6 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 7 | midi.NoteOffEvent(tick=4, channel=0, data=[64, 0]), 8 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 9 | midi.NoteOffEvent(tick=4, channel=0, data=[64, 0]), 10 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 11 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 12 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 13 | midi.NoteOffEvent(tick=4, channel=0, data=[62, 0]), 14 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 15 | midi.NoteOffEvent(tick=4, channel=0, data=[67, 0]), 16 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 17 | midi.NoteOffEvent(tick=4, channel=0, data=[67, 0]), 18 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 19 | midi.NoteOffEvent(tick=4, channel=0, data=[62, 0]), 20 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 21 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 22 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 23 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 24 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 25 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 26 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 27 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 28 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 29 | midi.NoteOffEvent(tick=4, channel=0, data=[59, 0]), 30 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 31 | midi.NoteOffEvent(tick=4, channel=0, data=[64, 0]), 32 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 33 | midi.NoteOffEvent(tick=4, channel=0, data=[64, 0]), 34 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 35 | midi.NoteOffEvent(tick=4, channel=0, data=[59, 0]), 36 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 37 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 38 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 39 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 40 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 41 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 42 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 43 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 44 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 45 | midi.NoteOffEvent(tick=4, channel=0, data=[55, 0]), 46 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 47 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 48 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 49 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 50 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 51 | midi.NoteOffEvent(tick=4, channel=0, data=[55, 0]), 52 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 53 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 54 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 55 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 56 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 57 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 58 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 59 | midi.NoteOffEvent(tick=4, channel=0, data=[62, 0]), 60 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 61 | midi.NoteOffEvent(tick=8, channel=0, data=[62, 0]), 62 | midi.NoteOnEvent(tick=8, channel=0, data=[64, 120]), 63 | midi.NoteOffEvent(tick=4, channel=0, data=[64, 0]), 64 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 65 | midi.NoteOffEvent(tick=4, channel=0, data=[62, 0]), 66 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 67 | midi.NoteOffEvent(tick=2, channel=0, data=[60, 0]), 68 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 69 | midi.NoteOffEvent(tick=2, channel=0, data=[62, 0]), 70 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 71 | midi.NoteOffEvent(tick=4, channel=0, data=[64, 0]), 72 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 73 | midi.NoteOffEvent(tick=4, channel=0, data=[62, 0]), 74 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 75 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 76 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 77 | midi.NoteOffEvent(tick=8, channel=0, data=[59, 0]), 78 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 79 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 80 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 81 | midi.NoteOffEvent(tick=4, channel=0, data=[59, 0]), 82 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 83 | midi.NoteOffEvent(tick=2, channel=0, data=[57, 0]), 84 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 85 | midi.NoteOffEvent(tick=2, channel=0, data=[59, 0]), 86 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 87 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 88 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 89 | midi.NoteOffEvent(tick=4, channel=0, data=[59, 0]), 90 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 91 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 92 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 93 | midi.NoteOffEvent(tick=8, channel=0, data=[55, 0]), 94 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 95 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 96 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 97 | midi.NoteOffEvent(tick=4, channel=0, data=[55, 0]), 98 | midi.NoteOnEvent(tick=0, channel=0, data=[53, 120]), 99 | midi.NoteOffEvent(tick=2, channel=0, data=[53, 0]), 100 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 101 | midi.NoteOffEvent(tick=2, channel=0, data=[55, 0]), 102 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 103 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 104 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 105 | midi.NoteOffEvent(tick=4, channel=0, data=[55, 0]), 106 | midi.NoteOnEvent(tick=0, channel=0, data=[53, 120]), 107 | midi.NoteOffEvent(tick=4, channel=0, data=[53, 0]), 108 | midi.NoteOnEvent(tick=0, channel=0, data=[52, 120]), 109 | midi.NoteOffEvent(tick=4, channel=0, data=[52, 0]), 110 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 111 | midi.NoteOffEvent(tick=4, channel=0, data=[55, 0]), 112 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 113 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 114 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 115 | midi.NoteOffEvent(tick=4, channel=0, data=[55, 0]), 116 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 117 | midi.NoteOffEvent(tick=2, channel=0, data=[57, 0]), 118 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 119 | midi.NoteOffEvent(tick=2, channel=0, data=[59, 0]), 120 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 121 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 122 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 123 | midi.NoteOffEvent(tick=12, channel=0, data=[62, 0]), 124 | midi.EndOfTrackEvent(tick=1, data=[])], 125 | [midi.SetTempoEvent(tick=0, data=[6, 26, 128]), 126 | midi.ProgramChangeEvent(tick=0, channel=1, data=[40]), 127 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 128 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 129 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 130 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 131 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 132 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 133 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 134 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 135 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 136 | midi.NoteOffEvent(tick=16, channel=1, data=[55, 0]), 137 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 138 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 139 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 140 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 141 | midi.NoteOnEvent(tick=0, channel=1, data=[64, 50]), 142 | midi.NoteOffEvent(tick=16, channel=1, data=[57, 0]), 143 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 144 | midi.NoteOffEvent(tick=0, channel=1, data=[64, 0]), 145 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 146 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 147 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 148 | midi.NoteOffEvent(tick=16, channel=1, data=[52, 0]), 149 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 150 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 151 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 152 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 153 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 154 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 155 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 156 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 157 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 158 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 159 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 160 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 161 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 162 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 163 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 164 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 165 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 166 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 167 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 168 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 169 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 170 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 171 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 172 | midi.NoteOffEvent(tick=16, channel=1, data=[55, 0]), 173 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 174 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 175 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 176 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 177 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 178 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 179 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 180 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 181 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 182 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 183 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 184 | midi.NoteOffEvent(tick=16, channel=1, data=[55, 0]), 185 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 186 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 187 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 188 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 189 | midi.NoteOnEvent(tick=0, channel=1, data=[64, 50]), 190 | midi.NoteOffEvent(tick=16, channel=1, data=[57, 0]), 191 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 192 | midi.NoteOffEvent(tick=0, channel=1, data=[64, 0]), 193 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 194 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 195 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 196 | midi.NoteOffEvent(tick=16, channel=1, data=[52, 0]), 197 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 198 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 199 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 200 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 201 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 202 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 203 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 204 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 205 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 206 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 207 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 208 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 209 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 210 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 211 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 212 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 213 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 214 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 215 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 216 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 217 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 218 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 219 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 220 | midi.NoteOffEvent(tick=12, channel=1, data=[55, 0]), 221 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 222 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 223 | midi.EndOfTrackEvent(tick=1, data=[])], 224 | [midi.SetTempoEvent(tick=0, data=[6, 26, 128]), 225 | midi.ProgramChangeEvent(tick=0, channel=2, data=[19]), 226 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 227 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 228 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 229 | midi.NoteOffEvent(tick=16, channel=2, data=[43, 0]), 230 | midi.NoteOnEvent(tick=0, channel=2, data=[45, 100]), 231 | midi.NoteOffEvent(tick=16, channel=2, data=[45, 0]), 232 | midi.NoteOnEvent(tick=0, channel=2, data=[40, 100]), 233 | midi.NoteOffEvent(tick=16, channel=2, data=[40, 0]), 234 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 235 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 236 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 237 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 238 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 239 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 240 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 241 | midi.NoteOffEvent(tick=16, channel=2, data=[43, 0]), 242 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 243 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 244 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 245 | midi.NoteOffEvent(tick=16, channel=2, data=[43, 0]), 246 | midi.NoteOnEvent(tick=0, channel=2, data=[45, 100]), 247 | midi.NoteOffEvent(tick=16, channel=2, data=[45, 0]), 248 | midi.NoteOnEvent(tick=0, channel=2, data=[40, 100]), 249 | midi.NoteOffEvent(tick=16, channel=2, data=[40, 0]), 250 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 251 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 252 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 253 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 254 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 255 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 256 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 257 | midi.NoteOffEvent(tick=12, channel=2, data=[43, 0]), 258 | midi.EndOfTrackEvent(tick=1, data=[])]]) -------------------------------------------------------------------------------- /tests/regression/expected/canon_riff_tie_notes.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/tests/regression/expected/canon_riff_tie_notes.song.mid -------------------------------------------------------------------------------- /tests/regression/expected/canon_riff_tie_notes.song.mid.dump: -------------------------------------------------------------------------------- 1 | midi.Pattern(format=1, resolution=4, tracks=\ 2 | [[midi.SetTempoEvent(tick=0, data=[5, 98, 173]), 3 | midi.ProgramChangeEvent(tick=0, channel=0, data=[73]), 4 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 5 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 6 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 7 | midi.NoteOffEvent(tick=4, channel=0, data=[64, 0]), 8 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 9 | midi.NoteOffEvent(tick=4, channel=0, data=[64, 0]), 10 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 11 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 12 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 13 | midi.NoteOffEvent(tick=4, channel=0, data=[62, 0]), 14 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 15 | midi.NoteOffEvent(tick=4, channel=0, data=[67, 0]), 16 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 17 | midi.NoteOffEvent(tick=4, channel=0, data=[67, 0]), 18 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 19 | midi.NoteOffEvent(tick=4, channel=0, data=[62, 0]), 20 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 21 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 22 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 23 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 24 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 25 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 26 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 27 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 28 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 29 | midi.NoteOffEvent(tick=4, channel=0, data=[59, 0]), 30 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 31 | midi.NoteOffEvent(tick=4, channel=0, data=[64, 0]), 32 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 33 | midi.NoteOffEvent(tick=4, channel=0, data=[64, 0]), 34 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 35 | midi.NoteOffEvent(tick=4, channel=0, data=[59, 0]), 36 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 37 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 38 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 39 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 40 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 41 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 42 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 43 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 44 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 45 | midi.NoteOffEvent(tick=4, channel=0, data=[55, 0]), 46 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 47 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 48 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 49 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 50 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 51 | midi.NoteOffEvent(tick=4, channel=0, data=[55, 0]), 52 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 53 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 54 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 55 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 56 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 57 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 58 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 59 | midi.NoteOffEvent(tick=4, channel=0, data=[62, 0]), 60 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 61 | midi.NoteOffEvent(tick=8, channel=0, data=[62, 0]), 62 | midi.NoteOnEvent(tick=8, channel=0, data=[64, 120]), 63 | midi.NoteOffEvent(tick=4, channel=0, data=[64, 0]), 64 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 65 | midi.NoteOffEvent(tick=4, channel=0, data=[62, 0]), 66 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 67 | midi.NoteOffEvent(tick=2, channel=0, data=[60, 0]), 68 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 69 | midi.NoteOffEvent(tick=2, channel=0, data=[62, 0]), 70 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 71 | midi.NoteOffEvent(tick=2, channel=0, data=[64, 0]), 72 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 73 | midi.NoteOffEvent(tick=6, channel=0, data=[62, 0]), 74 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 75 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 76 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 77 | midi.NoteOffEvent(tick=8, channel=0, data=[59, 0]), 78 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 79 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 80 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 81 | midi.NoteOffEvent(tick=4, channel=0, data=[59, 0]), 82 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 83 | midi.NoteOffEvent(tick=2, channel=0, data=[57, 0]), 84 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 85 | midi.NoteOffEvent(tick=2, channel=0, data=[59, 0]), 86 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 87 | midi.NoteOffEvent(tick=2, channel=0, data=[60, 0]), 88 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 89 | midi.NoteOffEvent(tick=6, channel=0, data=[59, 0]), 90 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 91 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 92 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 93 | midi.NoteOffEvent(tick=8, channel=0, data=[55, 0]), 94 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 95 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 96 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 97 | midi.NoteOffEvent(tick=4, channel=0, data=[55, 0]), 98 | midi.NoteOnEvent(tick=0, channel=0, data=[53, 120]), 99 | midi.NoteOffEvent(tick=2, channel=0, data=[53, 0]), 100 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 101 | midi.NoteOffEvent(tick=2, channel=0, data=[55, 0]), 102 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 103 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 104 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 105 | midi.NoteOffEvent(tick=4, channel=0, data=[55, 0]), 106 | midi.NoteOnEvent(tick=0, channel=0, data=[53, 120]), 107 | midi.NoteOffEvent(tick=4, channel=0, data=[53, 0]), 108 | midi.NoteOnEvent(tick=0, channel=0, data=[52, 120]), 109 | midi.NoteOffEvent(tick=4, channel=0, data=[52, 0]), 110 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 111 | midi.NoteOffEvent(tick=4, channel=0, data=[55, 0]), 112 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 113 | midi.NoteOffEvent(tick=4, channel=0, data=[57, 0]), 114 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 115 | midi.NoteOffEvent(tick=4, channel=0, data=[55, 0]), 116 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 117 | midi.NoteOffEvent(tick=2, channel=0, data=[57, 0]), 118 | midi.NoteOnEvent(tick=0, channel=0, data=[59, 120]), 119 | midi.NoteOffEvent(tick=2, channel=0, data=[59, 0]), 120 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 121 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 122 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 123 | midi.NoteOffEvent(tick=12, channel=0, data=[62, 0]), 124 | midi.EndOfTrackEvent(tick=1, data=[])], 125 | [midi.SetTempoEvent(tick=0, data=[5, 98, 173]), 126 | midi.ProgramChangeEvent(tick=0, channel=1, data=[40]), 127 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 128 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 129 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 130 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 131 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 132 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 133 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 134 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 135 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 136 | midi.NoteOffEvent(tick=16, channel=1, data=[55, 0]), 137 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 138 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 139 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 140 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 141 | midi.NoteOnEvent(tick=0, channel=1, data=[64, 50]), 142 | midi.NoteOffEvent(tick=16, channel=1, data=[57, 0]), 143 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 144 | midi.NoteOffEvent(tick=0, channel=1, data=[64, 0]), 145 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 146 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 147 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 148 | midi.NoteOffEvent(tick=16, channel=1, data=[52, 0]), 149 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 150 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 151 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 152 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 153 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 154 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 155 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 156 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 157 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 158 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 159 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 160 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 161 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 162 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 163 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 164 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 165 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 166 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 167 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 168 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 169 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 170 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 171 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 172 | midi.NoteOffEvent(tick=16, channel=1, data=[55, 0]), 173 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 174 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 175 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 176 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 177 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 178 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 179 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 180 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 181 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 182 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 183 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 184 | midi.NoteOffEvent(tick=16, channel=1, data=[55, 0]), 185 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 186 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 187 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 188 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 189 | midi.NoteOnEvent(tick=0, channel=1, data=[64, 50]), 190 | midi.NoteOffEvent(tick=16, channel=1, data=[57, 0]), 191 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 192 | midi.NoteOffEvent(tick=0, channel=1, data=[64, 0]), 193 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 194 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 195 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 196 | midi.NoteOffEvent(tick=16, channel=1, data=[52, 0]), 197 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 198 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 199 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 200 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 201 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 202 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 203 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 204 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 205 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 206 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 207 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 208 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 209 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 210 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 211 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 212 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 213 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 214 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 215 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 216 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 217 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 218 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 219 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 220 | midi.NoteOffEvent(tick=12, channel=1, data=[55, 0]), 221 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 222 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 223 | midi.EndOfTrackEvent(tick=1, data=[])], 224 | [midi.SetTempoEvent(tick=0, data=[5, 98, 173]), 225 | midi.ProgramChangeEvent(tick=0, channel=2, data=[19]), 226 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 227 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 228 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 229 | midi.NoteOffEvent(tick=16, channel=2, data=[43, 0]), 230 | midi.NoteOnEvent(tick=0, channel=2, data=[45, 100]), 231 | midi.NoteOffEvent(tick=16, channel=2, data=[45, 0]), 232 | midi.NoteOnEvent(tick=0, channel=2, data=[40, 100]), 233 | midi.NoteOffEvent(tick=16, channel=2, data=[40, 0]), 234 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 235 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 236 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 237 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 238 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 239 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 240 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 241 | midi.NoteOffEvent(tick=16, channel=2, data=[43, 0]), 242 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 243 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 244 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 245 | midi.NoteOffEvent(tick=16, channel=2, data=[43, 0]), 246 | midi.NoteOnEvent(tick=0, channel=2, data=[45, 100]), 247 | midi.NoteOffEvent(tick=16, channel=2, data=[45, 0]), 248 | midi.NoteOnEvent(tick=0, channel=2, data=[40, 100]), 249 | midi.NoteOffEvent(tick=16, channel=2, data=[40, 0]), 250 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 251 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 252 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 253 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 254 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 255 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 256 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 257 | midi.NoteOffEvent(tick=12, channel=2, data=[43, 0]), 258 | midi.EndOfTrackEvent(tick=1, data=[])]]) -------------------------------------------------------------------------------- /tests/regression/expected/clementine.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/tests/regression/expected/clementine.song.mid -------------------------------------------------------------------------------- /tests/regression/expected/clementine.song.mid.dump: -------------------------------------------------------------------------------- 1 | midi.Pattern(format=1, resolution=4, tracks=\ 2 | [[midi.SetTempoEvent(tick=0, data=[9, 39, 192]), 3 | midi.ProgramChangeEvent(tick=0, channel=0, data=[73]), 4 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 5 | midi.NoteOffEvent(tick=3, channel=0, data=[72, 0]), 6 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 7 | midi.NoteOffEvent(tick=1, channel=0, data=[72, 0]), 8 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 9 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 10 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 11 | midi.NoteOffEvent(tick=4, channel=0, data=[67, 0]), 12 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 13 | midi.NoteOffEvent(tick=3, channel=0, data=[76, 0]), 14 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 15 | midi.NoteOffEvent(tick=1, channel=0, data=[76, 0]), 16 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 17 | midi.NoteOffEvent(tick=4, channel=0, data=[76, 0]), 18 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 19 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 20 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 21 | midi.NoteOffEvent(tick=3, channel=0, data=[72, 0]), 22 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 23 | midi.NoteOffEvent(tick=1, channel=0, data=[76, 0]), 24 | midi.NoteOnEvent(tick=0, channel=0, data=[79, 120]), 25 | midi.NoteOffEvent(tick=6, channel=0, data=[79, 0]), 26 | midi.NoteOnEvent(tick=0, channel=0, data=[79, 120]), 27 | midi.NoteOffEvent(tick=2, channel=0, data=[79, 0]), 28 | midi.NoteOnEvent(tick=0, channel=0, data=[77, 120]), 29 | midi.NoteOffEvent(tick=2, channel=0, data=[77, 0]), 30 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 31 | midi.NoteOffEvent(tick=2, channel=0, data=[76, 0]), 32 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 120]), 33 | midi.NoteOffEvent(tick=8, channel=0, data=[74, 0]), 34 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 120]), 35 | midi.NoteOffEvent(tick=3, channel=0, data=[74, 0]), 36 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 37 | midi.NoteOffEvent(tick=1, channel=0, data=[76, 0]), 38 | midi.NoteOnEvent(tick=0, channel=0, data=[77, 120]), 39 | midi.NoteOffEvent(tick=4, channel=0, data=[77, 0]), 40 | midi.NoteOnEvent(tick=0, channel=0, data=[77, 120]), 41 | midi.NoteOffEvent(tick=4, channel=0, data=[77, 0]), 42 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 43 | midi.NoteOffEvent(tick=3, channel=0, data=[76, 0]), 44 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 120]), 45 | midi.NoteOffEvent(tick=1, channel=0, data=[74, 0]), 46 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 47 | midi.NoteOffEvent(tick=4, channel=0, data=[76, 0]), 48 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 49 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 50 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 51 | midi.NoteOffEvent(tick=3, channel=0, data=[72, 0]), 52 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 53 | midi.NoteOffEvent(tick=1, channel=0, data=[76, 0]), 54 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 120]), 55 | midi.NoteOffEvent(tick=6, channel=0, data=[74, 0]), 56 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 57 | midi.NoteOffEvent(tick=2, channel=0, data=[67, 0]), 58 | midi.NoteOnEvent(tick=0, channel=0, data=[71, 120]), 59 | midi.NoteOffEvent(tick=3, channel=0, data=[71, 0]), 60 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 120]), 61 | midi.NoteOffEvent(tick=1, channel=0, data=[74, 0]), 62 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 63 | midi.NoteOffEvent(tick=8, channel=0, data=[72, 0]), 64 | midi.EndOfTrackEvent(tick=1, data=[])], 65 | [midi.SetTempoEvent(tick=0, data=[9, 39, 192]), 66 | midi.ProgramChangeEvent(tick=0, channel=1, data=[40]), 67 | midi.NoteOnEvent(tick=4, channel=1, data=[48, 50]), 68 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 69 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 70 | midi.NoteOffEvent(tick=12, channel=1, data=[48, 0]), 71 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 72 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 73 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 74 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 75 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 76 | midi.NoteOffEvent(tick=12, channel=1, data=[48, 0]), 77 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 78 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 79 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 80 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 81 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 82 | midi.NoteOffEvent(tick=12, channel=1, data=[48, 0]), 83 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 84 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 85 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 86 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 87 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 88 | midi.NoteOnEvent(tick=0, channel=1, data=[65, 50]), 89 | midi.NoteOffEvent(tick=12, channel=1, data=[55, 0]), 90 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 91 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 92 | midi.NoteOffEvent(tick=0, channel=1, data=[65, 0]), 93 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 94 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 95 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 96 | midi.NoteOffEvent(tick=12, channel=1, data=[53, 0]), 97 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 98 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 99 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 100 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 101 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 102 | midi.NoteOffEvent(tick=12, channel=1, data=[48, 0]), 103 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 104 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 105 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 106 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 107 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 108 | midi.NoteOnEvent(tick=0, channel=1, data=[65, 50]), 109 | midi.NoteOffEvent(tick=12, channel=1, data=[55, 0]), 110 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 111 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 112 | midi.NoteOffEvent(tick=0, channel=1, data=[65, 0]), 113 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 114 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 115 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 116 | midi.NoteOffEvent(tick=8, channel=1, data=[48, 0]), 117 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 118 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 119 | midi.EndOfTrackEvent(tick=1, data=[])], 120 | [midi.SetTempoEvent(tick=0, data=[9, 39, 192]), 121 | midi.ProgramChangeEvent(tick=0, channel=2, data=[19]), 122 | midi.NoteOnEvent(tick=4, channel=2, data=[36, 100]), 123 | midi.NoteOffEvent(tick=12, channel=2, data=[36, 0]), 124 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 125 | midi.NoteOffEvent(tick=12, channel=2, data=[36, 0]), 126 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 127 | midi.NoteOffEvent(tick=12, channel=2, data=[36, 0]), 128 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 129 | midi.NoteOffEvent(tick=12, channel=2, data=[43, 0]), 130 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 131 | midi.NoteOffEvent(tick=12, channel=2, data=[41, 0]), 132 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 133 | midi.NoteOffEvent(tick=12, channel=2, data=[36, 0]), 134 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 135 | midi.NoteOffEvent(tick=12, channel=2, data=[43, 0]), 136 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 137 | midi.NoteOffEvent(tick=8, channel=2, data=[36, 0]), 138 | midi.EndOfTrackEvent(tick=1, data=[])]]) -------------------------------------------------------------------------------- /tests/regression/expected/silent_night.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/tests/regression/expected/silent_night.song.mid -------------------------------------------------------------------------------- /tests/regression/expected/silent_night.song.mid.dump: -------------------------------------------------------------------------------- 1 | midi.Pattern(format=1, resolution=8, tracks=\ 2 | [[midi.SetTempoEvent(tick=0, data=[8, 82, 174]), 3 | midi.ProgramChangeEvent(tick=0, channel=0, data=[73]), 4 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 5 | midi.NoteOffEvent(tick=13, channel=0, data=[60, 0]), 6 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 7 | midi.NoteOffEvent(tick=3, channel=0, data=[62, 0]), 8 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 9 | midi.NoteOffEvent(tick=8, channel=0, data=[60, 0]), 10 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 11 | midi.NoteOffEvent(tick=16, channel=0, data=[57, 0]), 12 | midi.NoteOnEvent(tick=8, channel=0, data=[60, 120]), 13 | midi.NoteOffEvent(tick=13, channel=0, data=[60, 0]), 14 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 15 | midi.NoteOffEvent(tick=3, channel=0, data=[62, 0]), 16 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 17 | midi.NoteOffEvent(tick=8, channel=0, data=[60, 0]), 18 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 19 | midi.NoteOffEvent(tick=16, channel=0, data=[57, 0]), 20 | midi.NoteOnEvent(tick=8, channel=0, data=[67, 120]), 21 | midi.NoteOffEvent(tick=16, channel=0, data=[67, 0]), 22 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 23 | midi.NoteOffEvent(tick=8, channel=0, data=[67, 0]), 24 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 25 | midi.NoteOffEvent(tick=16, channel=0, data=[64, 0]), 26 | midi.NoteOnEvent(tick=8, channel=0, data=[65, 120]), 27 | midi.NoteOffEvent(tick=16, channel=0, data=[65, 0]), 28 | midi.NoteOnEvent(tick=0, channel=0, data=[65, 120]), 29 | midi.NoteOffEvent(tick=8, channel=0, data=[65, 0]), 30 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 31 | midi.NoteOffEvent(tick=16, channel=0, data=[60, 0]), 32 | midi.NoteOnEvent(tick=8, channel=0, data=[62, 120]), 33 | midi.NoteOffEvent(tick=16, channel=0, data=[62, 0]), 34 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 35 | midi.NoteOffEvent(tick=8, channel=0, data=[62, 0]), 36 | midi.NoteOnEvent(tick=0, channel=0, data=[65, 120]), 37 | midi.NoteOffEvent(tick=13, channel=0, data=[65, 0]), 38 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 39 | midi.NoteOffEvent(tick=3, channel=0, data=[64, 0]), 40 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 41 | midi.NoteOffEvent(tick=8, channel=0, data=[62, 0]), 42 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 43 | midi.NoteOffEvent(tick=13, channel=0, data=[60, 0]), 44 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 45 | midi.NoteOffEvent(tick=3, channel=0, data=[62, 0]), 46 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 47 | midi.NoteOffEvent(tick=8, channel=0, data=[60, 0]), 48 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 49 | midi.NoteOffEvent(tick=16, channel=0, data=[57, 0]), 50 | midi.NoteOnEvent(tick=8, channel=0, data=[62, 120]), 51 | midi.NoteOffEvent(tick=16, channel=0, data=[62, 0]), 52 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 53 | midi.NoteOffEvent(tick=8, channel=0, data=[62, 0]), 54 | midi.NoteOnEvent(tick=0, channel=0, data=[65, 120]), 55 | midi.NoteOffEvent(tick=13, channel=0, data=[65, 0]), 56 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 57 | midi.NoteOffEvent(tick=3, channel=0, data=[64, 0]), 58 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 59 | midi.NoteOffEvent(tick=8, channel=0, data=[62, 0]), 60 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 61 | midi.NoteOffEvent(tick=13, channel=0, data=[60, 0]), 62 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 63 | midi.NoteOffEvent(tick=3, channel=0, data=[62, 0]), 64 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 65 | midi.NoteOffEvent(tick=8, channel=0, data=[60, 0]), 66 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 67 | midi.NoteOffEvent(tick=16, channel=0, data=[57, 0]), 68 | midi.NoteOnEvent(tick=8, channel=0, data=[67, 120]), 69 | midi.NoteOffEvent(tick=16, channel=0, data=[67, 0]), 70 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 71 | midi.NoteOffEvent(tick=8, channel=0, data=[67, 0]), 72 | midi.NoteOnEvent(tick=0, channel=0, data=[70, 120]), 73 | midi.NoteOffEvent(tick=13, channel=0, data=[70, 0]), 74 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 75 | midi.NoteOffEvent(tick=3, channel=0, data=[67, 0]), 76 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 77 | midi.NoteOffEvent(tick=8, channel=0, data=[64, 0]), 78 | midi.NoteOnEvent(tick=0, channel=0, data=[65, 120]), 79 | midi.NoteOffEvent(tick=24, channel=0, data=[65, 0]), 80 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 120]), 81 | midi.NoteOffEvent(tick=16, channel=0, data=[69, 0]), 82 | midi.NoteOnEvent(tick=8, channel=0, data=[65, 120]), 83 | midi.NoteOffEvent(tick=8, channel=0, data=[65, 0]), 84 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 85 | midi.NoteOffEvent(tick=8, channel=0, data=[60, 0]), 86 | midi.NoteOnEvent(tick=0, channel=0, data=[57, 120]), 87 | midi.NoteOffEvent(tick=8, channel=0, data=[57, 0]), 88 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 89 | midi.NoteOffEvent(tick=13, channel=0, data=[60, 0]), 90 | midi.NoteOnEvent(tick=0, channel=0, data=[58, 120]), 91 | midi.NoteOffEvent(tick=3, channel=0, data=[58, 0]), 92 | midi.NoteOnEvent(tick=0, channel=0, data=[55, 120]), 93 | midi.NoteOffEvent(tick=8, channel=0, data=[55, 0]), 94 | midi.NoteOnEvent(tick=0, channel=0, data=[53, 120]), 95 | midi.NoteOffEvent(tick=16, channel=0, data=[53, 0]), 96 | midi.EndOfTrackEvent(tick=1, data=[])], 97 | [midi.SetTempoEvent(tick=0, data=[8, 82, 174]), 98 | midi.ProgramChangeEvent(tick=0, channel=1, data=[40]), 99 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 100 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 101 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 102 | midi.NoteOffEvent(tick=48, channel=1, data=[53, 0]), 103 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 104 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 105 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 106 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 107 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 108 | midi.NoteOffEvent(tick=48, channel=1, data=[53, 0]), 109 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 110 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 111 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 112 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 113 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 114 | midi.NoteOffEvent(tick=48, channel=1, data=[48, 0]), 115 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 116 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 117 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 118 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 119 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 120 | midi.NoteOffEvent(tick=48, channel=1, data=[53, 0]), 121 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 122 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 123 | midi.NoteOnEvent(tick=0, channel=1, data=[58, 50]), 124 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 125 | midi.NoteOnEvent(tick=0, channel=1, data=[65, 50]), 126 | midi.NoteOffEvent(tick=48, channel=1, data=[58, 0]), 127 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 128 | midi.NoteOffEvent(tick=0, channel=1, data=[65, 0]), 129 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 130 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 131 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 132 | midi.NoteOffEvent(tick=48, channel=1, data=[53, 0]), 133 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 134 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 135 | midi.NoteOnEvent(tick=0, channel=1, data=[58, 50]), 136 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 137 | midi.NoteOnEvent(tick=0, channel=1, data=[65, 50]), 138 | midi.NoteOffEvent(tick=48, channel=1, data=[58, 0]), 139 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 140 | midi.NoteOffEvent(tick=0, channel=1, data=[65, 0]), 141 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 142 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 143 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 144 | midi.NoteOffEvent(tick=48, channel=1, data=[53, 0]), 145 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 146 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 147 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 148 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 149 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 150 | midi.NoteOffEvent(tick=48, channel=1, data=[48, 0]), 151 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 152 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 153 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 154 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 155 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 156 | midi.NoteOffEvent(tick=72, channel=1, data=[53, 0]), 157 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 158 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 159 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 160 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 161 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 162 | midi.NoteOffEvent(tick=24, channel=1, data=[48, 0]), 163 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 164 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 165 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 166 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 167 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 168 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 169 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 170 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 171 | midi.EndOfTrackEvent(tick=1, data=[])], 172 | [midi.SetTempoEvent(tick=0, data=[8, 82, 174]), 173 | midi.ProgramChangeEvent(tick=0, channel=2, data=[19]), 174 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 175 | midi.NoteOffEvent(tick=48, channel=2, data=[41, 0]), 176 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 177 | midi.NoteOffEvent(tick=48, channel=2, data=[41, 0]), 178 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 179 | midi.NoteOffEvent(tick=48, channel=2, data=[36, 0]), 180 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 181 | midi.NoteOffEvent(tick=48, channel=2, data=[41, 0]), 182 | midi.NoteOnEvent(tick=0, channel=2, data=[46, 100]), 183 | midi.NoteOffEvent(tick=48, channel=2, data=[46, 0]), 184 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 185 | midi.NoteOffEvent(tick=48, channel=2, data=[41, 0]), 186 | midi.NoteOnEvent(tick=0, channel=2, data=[46, 100]), 187 | midi.NoteOffEvent(tick=48, channel=2, data=[46, 0]), 188 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 189 | midi.NoteOffEvent(tick=48, channel=2, data=[41, 0]), 190 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 191 | midi.NoteOffEvent(tick=48, channel=2, data=[36, 0]), 192 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 193 | midi.NoteOffEvent(tick=72, channel=2, data=[41, 0]), 194 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 195 | midi.NoteOffEvent(tick=24, channel=2, data=[36, 0]), 196 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 197 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 198 | midi.EndOfTrackEvent(tick=1, data=[])]]) -------------------------------------------------------------------------------- /tests/regression/expected/twinkle_twinkle_little_star.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/tests/regression/expected/twinkle_twinkle_little_star.song.mid -------------------------------------------------------------------------------- /tests/regression/expected/twinkle_twinkle_little_star.song.mid.dump: -------------------------------------------------------------------------------- 1 | midi.Pattern(format=1, resolution=4, tracks=\ 2 | [[midi.SetTempoEvent(tick=0, data=[15, 66, 64]), 3 | midi.ProgramChangeEvent(tick=0, channel=0, data=[73]), 4 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 5 | midi.NoteOffEvent(tick=2, channel=0, data=[60, 0]), 6 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 7 | midi.NoteOffEvent(tick=2, channel=0, data=[60, 0]), 8 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 9 | midi.NoteOffEvent(tick=2, channel=0, data=[67, 0]), 10 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 11 | midi.NoteOffEvent(tick=2, channel=0, data=[67, 0]), 12 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 120]), 13 | midi.NoteOffEvent(tick=2, channel=0, data=[69, 0]), 14 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 120]), 15 | midi.NoteOffEvent(tick=2, channel=0, data=[69, 0]), 16 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 17 | midi.NoteOffEvent(tick=4, channel=0, data=[67, 0]), 18 | midi.NoteOnEvent(tick=0, channel=0, data=[65, 120]), 19 | midi.NoteOffEvent(tick=2, channel=0, data=[65, 0]), 20 | midi.NoteOnEvent(tick=0, channel=0, data=[65, 120]), 21 | midi.NoteOffEvent(tick=2, channel=0, data=[65, 0]), 22 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 23 | midi.NoteOffEvent(tick=2, channel=0, data=[64, 0]), 24 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 25 | midi.NoteOffEvent(tick=2, channel=0, data=[64, 0]), 26 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 27 | midi.NoteOffEvent(tick=2, channel=0, data=[62, 0]), 28 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 29 | midi.NoteOffEvent(tick=2, channel=0, data=[62, 0]), 30 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 31 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 32 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 33 | midi.NoteOffEvent(tick=2, channel=0, data=[67, 0]), 34 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 35 | midi.NoteOffEvent(tick=2, channel=0, data=[67, 0]), 36 | midi.NoteOnEvent(tick=0, channel=0, data=[65, 120]), 37 | midi.NoteOffEvent(tick=2, channel=0, data=[65, 0]), 38 | midi.NoteOnEvent(tick=0, channel=0, data=[65, 120]), 39 | midi.NoteOffEvent(tick=2, channel=0, data=[65, 0]), 40 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 41 | midi.NoteOffEvent(tick=2, channel=0, data=[64, 0]), 42 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 43 | midi.NoteOffEvent(tick=2, channel=0, data=[64, 0]), 44 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 45 | midi.NoteOffEvent(tick=4, channel=0, data=[62, 0]), 46 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 47 | midi.NoteOffEvent(tick=2, channel=0, data=[67, 0]), 48 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 49 | midi.NoteOffEvent(tick=2, channel=0, data=[67, 0]), 50 | midi.NoteOnEvent(tick=0, channel=0, data=[65, 120]), 51 | midi.NoteOffEvent(tick=2, channel=0, data=[65, 0]), 52 | midi.NoteOnEvent(tick=0, channel=0, data=[65, 120]), 53 | midi.NoteOffEvent(tick=2, channel=0, data=[65, 0]), 54 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 55 | midi.NoteOffEvent(tick=2, channel=0, data=[64, 0]), 56 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 57 | midi.NoteOffEvent(tick=2, channel=0, data=[64, 0]), 58 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 59 | midi.NoteOffEvent(tick=4, channel=0, data=[62, 0]), 60 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 61 | midi.NoteOffEvent(tick=2, channel=0, data=[60, 0]), 62 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 63 | midi.NoteOffEvent(tick=2, channel=0, data=[60, 0]), 64 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 65 | midi.NoteOffEvent(tick=2, channel=0, data=[67, 0]), 66 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 67 | midi.NoteOffEvent(tick=2, channel=0, data=[67, 0]), 68 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 120]), 69 | midi.NoteOffEvent(tick=2, channel=0, data=[69, 0]), 70 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 120]), 71 | midi.NoteOffEvent(tick=2, channel=0, data=[69, 0]), 72 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 73 | midi.NoteOffEvent(tick=4, channel=0, data=[67, 0]), 74 | midi.NoteOnEvent(tick=0, channel=0, data=[65, 120]), 75 | midi.NoteOffEvent(tick=2, channel=0, data=[65, 0]), 76 | midi.NoteOnEvent(tick=0, channel=0, data=[65, 120]), 77 | midi.NoteOffEvent(tick=2, channel=0, data=[65, 0]), 78 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 79 | midi.NoteOffEvent(tick=2, channel=0, data=[64, 0]), 80 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 81 | midi.NoteOffEvent(tick=2, channel=0, data=[64, 0]), 82 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 83 | midi.NoteOffEvent(tick=2, channel=0, data=[62, 0]), 84 | midi.NoteOnEvent(tick=0, channel=0, data=[62, 120]), 85 | midi.NoteOffEvent(tick=2, channel=0, data=[62, 0]), 86 | midi.NoteOnEvent(tick=0, channel=0, data=[60, 120]), 87 | midi.NoteOffEvent(tick=4, channel=0, data=[60, 0]), 88 | midi.EndOfTrackEvent(tick=1, data=[])], 89 | [midi.SetTempoEvent(tick=0, data=[15, 66, 64]), 90 | midi.ProgramChangeEvent(tick=0, channel=1, data=[40]), 91 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 92 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 93 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 94 | midi.NoteOffEvent(tick=8, channel=1, data=[48, 0]), 95 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 96 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 97 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 98 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 99 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 100 | midi.NoteOffEvent(tick=4, channel=1, data=[53, 0]), 101 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 102 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 103 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 104 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 105 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 106 | midi.NoteOffEvent(tick=4, channel=1, data=[48, 0]), 107 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 108 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 109 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 110 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 111 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 112 | midi.NoteOffEvent(tick=4, channel=1, data=[53, 0]), 113 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 114 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 115 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 116 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 117 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 118 | midi.NoteOffEvent(tick=4, channel=1, data=[48, 0]), 119 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 120 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 121 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 122 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 123 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 124 | midi.NoteOffEvent(tick=4, channel=1, data=[55, 0]), 125 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 126 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 127 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 128 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 129 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 130 | midi.NoteOffEvent(tick=4, channel=1, data=[48, 0]), 131 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 132 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 133 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 134 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 135 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 136 | midi.NoteOffEvent(tick=4, channel=1, data=[48, 0]), 137 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 138 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 139 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 140 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 141 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 142 | midi.NoteOffEvent(tick=4, channel=1, data=[53, 0]), 143 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 144 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 145 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 146 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 147 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 148 | midi.NoteOffEvent(tick=4, channel=1, data=[48, 0]), 149 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 150 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 151 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 152 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 153 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 154 | midi.NoteOffEvent(tick=4, channel=1, data=[55, 0]), 155 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 156 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 157 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 158 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 159 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 160 | midi.NoteOffEvent(tick=4, channel=1, data=[48, 0]), 161 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 162 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 163 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 164 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 165 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 166 | midi.NoteOffEvent(tick=4, channel=1, data=[53, 0]), 167 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 168 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 169 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 170 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 171 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 172 | midi.NoteOffEvent(tick=4, channel=1, data=[48, 0]), 173 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 174 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 175 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 176 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 177 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 178 | midi.NoteOffEvent(tick=4, channel=1, data=[55, 0]), 179 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 180 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 181 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 182 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 183 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 184 | midi.NoteOffEvent(tick=8, channel=1, data=[48, 0]), 185 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 186 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 187 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 188 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 189 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 190 | midi.NoteOffEvent(tick=4, channel=1, data=[53, 0]), 191 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 192 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 193 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 194 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 195 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 196 | midi.NoteOffEvent(tick=4, channel=1, data=[48, 0]), 197 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 198 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 199 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 200 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 201 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 202 | midi.NoteOffEvent(tick=4, channel=1, data=[53, 0]), 203 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 204 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 205 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 206 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 207 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 208 | midi.NoteOffEvent(tick=4, channel=1, data=[48, 0]), 209 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 210 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 211 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 212 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 213 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 214 | midi.NoteOffEvent(tick=4, channel=1, data=[55, 0]), 215 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 216 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 217 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 218 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 219 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 220 | midi.NoteOffEvent(tick=4, channel=1, data=[48, 0]), 221 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 222 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 223 | midi.EndOfTrackEvent(tick=1, data=[])], 224 | [midi.SetTempoEvent(tick=0, data=[15, 66, 64]), 225 | midi.ProgramChangeEvent(tick=0, channel=2, data=[19]), 226 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 227 | midi.NoteOffEvent(tick=8, channel=2, data=[36, 0]), 228 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 229 | midi.NoteOffEvent(tick=4, channel=2, data=[41, 0]), 230 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 231 | midi.NoteOffEvent(tick=4, channel=2, data=[36, 0]), 232 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 233 | midi.NoteOffEvent(tick=4, channel=2, data=[41, 0]), 234 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 235 | midi.NoteOffEvent(tick=4, channel=2, data=[36, 0]), 236 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 237 | midi.NoteOffEvent(tick=4, channel=2, data=[43, 0]), 238 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 239 | midi.NoteOffEvent(tick=4, channel=2, data=[36, 0]), 240 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 241 | midi.NoteOffEvent(tick=4, channel=2, data=[36, 0]), 242 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 243 | midi.NoteOffEvent(tick=4, channel=2, data=[41, 0]), 244 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 245 | midi.NoteOffEvent(tick=4, channel=2, data=[36, 0]), 246 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 247 | midi.NoteOffEvent(tick=4, channel=2, data=[43, 0]), 248 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 249 | midi.NoteOffEvent(tick=4, channel=2, data=[36, 0]), 250 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 251 | midi.NoteOffEvent(tick=4, channel=2, data=[41, 0]), 252 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 253 | midi.NoteOffEvent(tick=4, channel=2, data=[36, 0]), 254 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 255 | midi.NoteOffEvent(tick=4, channel=2, data=[43, 0]), 256 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 257 | midi.NoteOffEvent(tick=8, channel=2, data=[36, 0]), 258 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 259 | midi.NoteOffEvent(tick=4, channel=2, data=[41, 0]), 260 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 261 | midi.NoteOffEvent(tick=4, channel=2, data=[36, 0]), 262 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 263 | midi.NoteOffEvent(tick=4, channel=2, data=[41, 0]), 264 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 265 | midi.NoteOffEvent(tick=4, channel=2, data=[36, 0]), 266 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 267 | midi.NoteOffEvent(tick=4, channel=2, data=[43, 0]), 268 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 269 | midi.NoteOffEvent(tick=4, channel=2, data=[36, 0]), 270 | midi.EndOfTrackEvent(tick=1, data=[])]]) -------------------------------------------------------------------------------- /tests/regression/expected/yankee_doodle.song.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdorrell/melody_scripter/4276a8afa2d9097d2206c3fb74e517417dedf652/tests/regression/expected/yankee_doodle.song.mid -------------------------------------------------------------------------------- /tests/regression/expected/yankee_doodle.song.mid.dump: -------------------------------------------------------------------------------- 1 | midi.Pattern(format=1, resolution=4, tracks=\ 2 | [[midi.SetTempoEvent(tick=0, data=[4, 147, 224]), 3 | midi.ProgramChangeEvent(tick=0, channel=0, data=[73]), 4 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 5 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 6 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 7 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 8 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 120]), 9 | midi.NoteOffEvent(tick=4, channel=0, data=[74, 0]), 10 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 11 | midi.NoteOffEvent(tick=4, channel=0, data=[76, 0]), 12 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 13 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 14 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 15 | midi.NoteOffEvent(tick=4, channel=0, data=[76, 0]), 16 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 120]), 17 | midi.NoteOffEvent(tick=4, channel=0, data=[74, 0]), 18 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 19 | midi.NoteOffEvent(tick=4, channel=0, data=[67, 0]), 20 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 21 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 22 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 23 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 24 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 120]), 25 | midi.NoteOffEvent(tick=4, channel=0, data=[74, 0]), 26 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 27 | midi.NoteOffEvent(tick=4, channel=0, data=[76, 0]), 28 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 29 | midi.NoteOffEvent(tick=8, channel=0, data=[72, 0]), 30 | midi.NoteOnEvent(tick=0, channel=0, data=[71, 120]), 31 | midi.NoteOffEvent(tick=8, channel=0, data=[71, 0]), 32 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 33 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 34 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 35 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 36 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 120]), 37 | midi.NoteOffEvent(tick=4, channel=0, data=[74, 0]), 38 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 39 | midi.NoteOffEvent(tick=4, channel=0, data=[76, 0]), 40 | midi.NoteOnEvent(tick=0, channel=0, data=[77, 120]), 41 | midi.NoteOffEvent(tick=4, channel=0, data=[77, 0]), 42 | midi.NoteOnEvent(tick=0, channel=0, data=[76, 120]), 43 | midi.NoteOffEvent(tick=4, channel=0, data=[76, 0]), 44 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 120]), 45 | midi.NoteOffEvent(tick=4, channel=0, data=[74, 0]), 46 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 47 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 48 | midi.NoteOnEvent(tick=0, channel=0, data=[71, 120]), 49 | midi.NoteOffEvent(tick=4, channel=0, data=[71, 0]), 50 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 51 | midi.NoteOffEvent(tick=4, channel=0, data=[67, 0]), 52 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 120]), 53 | midi.NoteOffEvent(tick=4, channel=0, data=[69, 0]), 54 | midi.NoteOnEvent(tick=0, channel=0, data=[71, 120]), 55 | midi.NoteOffEvent(tick=4, channel=0, data=[71, 0]), 56 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 57 | midi.NoteOffEvent(tick=8, channel=0, data=[72, 0]), 58 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 59 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 60 | midi.NoteOnEvent(tick=4, channel=0, data=[69, 120]), 61 | midi.NoteOffEvent(tick=6, channel=0, data=[69, 0]), 62 | midi.NoteOnEvent(tick=0, channel=0, data=[71, 120]), 63 | midi.NoteOffEvent(tick=2, channel=0, data=[71, 0]), 64 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 120]), 65 | midi.NoteOffEvent(tick=4, channel=0, data=[69, 0]), 66 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 67 | midi.NoteOffEvent(tick=4, channel=0, data=[67, 0]), 68 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 120]), 69 | midi.NoteOffEvent(tick=4, channel=0, data=[69, 0]), 70 | midi.NoteOnEvent(tick=0, channel=0, data=[71, 120]), 71 | midi.NoteOffEvent(tick=4, channel=0, data=[71, 0]), 72 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 73 | midi.NoteOffEvent(tick=8, channel=0, data=[72, 0]), 74 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 75 | midi.NoteOffEvent(tick=6, channel=0, data=[67, 0]), 76 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 120]), 77 | midi.NoteOffEvent(tick=2, channel=0, data=[69, 0]), 78 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 79 | midi.NoteOffEvent(tick=4, channel=0, data=[67, 0]), 80 | midi.NoteOnEvent(tick=0, channel=0, data=[65, 120]), 81 | midi.NoteOffEvent(tick=4, channel=0, data=[65, 0]), 82 | midi.NoteOnEvent(tick=0, channel=0, data=[64, 120]), 83 | midi.NoteOffEvent(tick=8, channel=0, data=[64, 0]), 84 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 85 | midi.NoteOffEvent(tick=8, channel=0, data=[67, 0]), 86 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 120]), 87 | midi.NoteOffEvent(tick=6, channel=0, data=[69, 0]), 88 | midi.NoteOnEvent(tick=0, channel=0, data=[71, 120]), 89 | midi.NoteOffEvent(tick=2, channel=0, data=[71, 0]), 90 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 120]), 91 | midi.NoteOffEvent(tick=4, channel=0, data=[69, 0]), 92 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 93 | midi.NoteOffEvent(tick=4, channel=0, data=[67, 0]), 94 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 120]), 95 | midi.NoteOffEvent(tick=4, channel=0, data=[69, 0]), 96 | midi.NoteOnEvent(tick=0, channel=0, data=[71, 120]), 97 | midi.NoteOffEvent(tick=4, channel=0, data=[71, 0]), 98 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 99 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 100 | midi.NoteOnEvent(tick=0, channel=0, data=[69, 120]), 101 | midi.NoteOffEvent(tick=4, channel=0, data=[69, 0]), 102 | midi.NoteOnEvent(tick=0, channel=0, data=[67, 120]), 103 | midi.NoteOffEvent(tick=4, channel=0, data=[67, 0]), 104 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 105 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 106 | midi.NoteOnEvent(tick=0, channel=0, data=[71, 120]), 107 | midi.NoteOffEvent(tick=4, channel=0, data=[71, 0]), 108 | midi.NoteOnEvent(tick=0, channel=0, data=[74, 120]), 109 | midi.NoteOffEvent(tick=4, channel=0, data=[74, 0]), 110 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 111 | midi.NoteOffEvent(tick=8, channel=0, data=[72, 0]), 112 | midi.NoteOnEvent(tick=0, channel=0, data=[72, 120]), 113 | midi.NoteOffEvent(tick=4, channel=0, data=[72, 0]), 114 | midi.EndOfTrackEvent(tick=1, data=[])], 115 | [midi.SetTempoEvent(tick=0, data=[4, 147, 224]), 116 | midi.ProgramChangeEvent(tick=0, channel=1, data=[40]), 117 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 118 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 119 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 120 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 121 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 122 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 123 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 124 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 125 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 126 | midi.NoteOffEvent(tick=8, channel=1, data=[48, 0]), 127 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 128 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 129 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 130 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 131 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 132 | midi.NoteOnEvent(tick=0, channel=1, data=[65, 50]), 133 | midi.NoteOffEvent(tick=8, channel=1, data=[55, 0]), 134 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 135 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 136 | midi.NoteOffEvent(tick=0, channel=1, data=[65, 0]), 137 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 138 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 139 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 140 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 141 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 142 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 143 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 144 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 145 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 146 | midi.NoteOffEvent(tick=8, channel=1, data=[48, 0]), 147 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 148 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 149 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 150 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 151 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 152 | midi.NoteOnEvent(tick=0, channel=1, data=[65, 50]), 153 | midi.NoteOffEvent(tick=8, channel=1, data=[55, 0]), 154 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 155 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 156 | midi.NoteOffEvent(tick=0, channel=1, data=[65, 0]), 157 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 158 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 159 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 160 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 161 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 162 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 163 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 164 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 165 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 166 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 167 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 168 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 169 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 170 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 171 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 172 | midi.NoteOnEvent(tick=0, channel=1, data=[65, 50]), 173 | midi.NoteOffEvent(tick=16, channel=1, data=[55, 0]), 174 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 175 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 176 | midi.NoteOffEvent(tick=0, channel=1, data=[65, 0]), 177 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 178 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 179 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 180 | midi.NoteOffEvent(tick=12, channel=1, data=[48, 0]), 181 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 182 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 183 | midi.NoteOnEvent(tick=4, channel=1, data=[53, 50]), 184 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 185 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 186 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 187 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 188 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 189 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 190 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 191 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 192 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 193 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 194 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 195 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 196 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 197 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 198 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 199 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 200 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 201 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 202 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 203 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 204 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 205 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 206 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 207 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 208 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 209 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 210 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 211 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 212 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 213 | midi.NoteOnEvent(tick=0, channel=1, data=[53, 50]), 214 | midi.NoteOnEvent(tick=0, channel=1, data=[57, 50]), 215 | midi.NoteOnEvent(tick=0, channel=1, data=[60, 50]), 216 | midi.NoteOffEvent(tick=16, channel=1, data=[53, 0]), 217 | midi.NoteOffEvent(tick=0, channel=1, data=[57, 0]), 218 | midi.NoteOffEvent(tick=0, channel=1, data=[60, 0]), 219 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 220 | midi.NoteOnEvent(tick=0, channel=1, data=[59, 50]), 221 | midi.NoteOnEvent(tick=0, channel=1, data=[62, 50]), 222 | midi.NoteOnEvent(tick=0, channel=1, data=[65, 50]), 223 | midi.NoteOffEvent(tick=16, channel=1, data=[55, 0]), 224 | midi.NoteOffEvent(tick=0, channel=1, data=[59, 0]), 225 | midi.NoteOffEvent(tick=0, channel=1, data=[62, 0]), 226 | midi.NoteOffEvent(tick=0, channel=1, data=[65, 0]), 227 | midi.NoteOnEvent(tick=0, channel=1, data=[48, 50]), 228 | midi.NoteOnEvent(tick=0, channel=1, data=[52, 50]), 229 | midi.NoteOnEvent(tick=0, channel=1, data=[55, 50]), 230 | midi.NoteOffEvent(tick=16, channel=1, data=[48, 0]), 231 | midi.NoteOffEvent(tick=0, channel=1, data=[52, 0]), 232 | midi.NoteOffEvent(tick=0, channel=1, data=[55, 0]), 233 | midi.EndOfTrackEvent(tick=1, data=[])], 234 | [midi.SetTempoEvent(tick=0, data=[4, 147, 224]), 235 | midi.ProgramChangeEvent(tick=0, channel=2, data=[19]), 236 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 237 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 238 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 239 | midi.NoteOffEvent(tick=8, channel=2, data=[36, 0]), 240 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 241 | midi.NoteOffEvent(tick=8, channel=2, data=[43, 0]), 242 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 243 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 244 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 245 | midi.NoteOffEvent(tick=8, channel=2, data=[36, 0]), 246 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 247 | midi.NoteOffEvent(tick=8, channel=2, data=[43, 0]), 248 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 249 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 250 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 251 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 252 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 253 | midi.NoteOffEvent(tick=16, channel=2, data=[43, 0]), 254 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 255 | midi.NoteOffEvent(tick=12, channel=2, data=[36, 0]), 256 | midi.NoteOnEvent(tick=4, channel=2, data=[41, 100]), 257 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 258 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 259 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 260 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 261 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 262 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 263 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 264 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 265 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 266 | midi.NoteOnEvent(tick=0, channel=2, data=[41, 100]), 267 | midi.NoteOffEvent(tick=16, channel=2, data=[41, 0]), 268 | midi.NoteOnEvent(tick=0, channel=2, data=[43, 100]), 269 | midi.NoteOffEvent(tick=16, channel=2, data=[43, 0]), 270 | midi.NoteOnEvent(tick=0, channel=2, data=[36, 100]), 271 | midi.NoteOffEvent(tick=16, channel=2, data=[36, 0]), 272 | midi.EndOfTrackEvent(tick=1, data=[])]]) -------------------------------------------------------------------------------- /tests/test_regression.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | 4 | from melody_scripter import song_parser 5 | from melody_scripter.song_parser import ParseException 6 | from melody_scripter.midi_song import compile_to_midi, dump_midi_file 7 | 8 | class RegressionTests(unittest.TestCase): 9 | 10 | def setUp(self): 11 | this_dir = os.path.dirname(__file__) 12 | self.songs_dir = os.path.abspath(os.path.join(this_dir, '..', 'data', 'songs')) 13 | self.output_dir = os.path.join(this_dir, 'regression', 'output') 14 | if not os.path.isdir(self.output_dir): os.makedirs(self.output_dir) 15 | self.expected_files_dir = os.path.join(this_dir, 'regression', 'expected') 16 | if not os.path.isdir(self.expected_files_dir): os.makedirs(self.expected_files_dir) 17 | 18 | def _read_binary_file(self, file_name): 19 | with open(file_name, mode = "rb") as f: 20 | return f.read() 21 | 22 | def _dump_midi_file(self, midi_file_name, dump_file_name): 23 | dump_string = dump_midi_file(midi_file_name) 24 | with open(dump_file_name, mode = "w") as f: 25 | f.write(dump_string) 26 | 27 | def _test_song_regression(self, file_name): 28 | song_file_name = os.path.join(self.songs_dir, file_name) 29 | expected_midi_file_name = os.path.join(self.expected_files_dir, file_name + ".mid") 30 | expected_midi_dump_file_name = expected_midi_file_name + ".dump" 31 | output_midi_file_name = os.path.join(self.output_dir, file_name + ".mid") 32 | output_midi_dump_file_name = output_midi_file_name + ".dump" 33 | if os.path.isfile(output_midi_file_name): 34 | os.remove(output_midi_file_name) 35 | try: 36 | compile_to_midi(song_file_name, output_midi_file_name) 37 | except ParseException, pe: 38 | pe.show_error() 39 | raise 40 | self._dump_midi_file(output_midi_file_name, output_midi_dump_file_name) 41 | output_midi = self._read_binary_file(output_midi_file_name) 42 | if os.path.isfile(expected_midi_file_name): 43 | expected_midi = self._read_binary_file(expected_midi_file_name) 44 | self._dump_midi_file(expected_midi_file_name, expected_midi_dump_file_name) 45 | if output_midi != expected_midi: 46 | self.fail('Midi file %s is different to expected %s' % (output_midi_file_name, expected_midi_file_name)) 47 | else: 48 | self.fail('Expected midi file %s does not exist (to compare with %s)' % (expected_midi_file_name, output_midi_file_name)) 49 | 50 | 51 | def test_song_regressions(self): 52 | print("") 53 | for file_name in os.listdir(self.songs_dir): 54 | if file_name.endswith('.song'): 55 | print(" regression test on song %s ..." % file_name) 56 | self._test_song_regression(file_name) 57 | -------------------------------------------------------------------------------- /tests/test_song_parser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from nose.tools import assert_raises 3 | import regex 4 | 5 | from melody_scripter import song_parser 6 | from melody_scripter.song_parser import FileToParse, LineToParse, LineRegionToParse, ParseException, StringToParse 7 | from melody_scripter.song_parser import Note, Rest, BarLine, Chord, SongItem, ScaleNote, SongItems, scale_note, Tie 8 | from melody_scripter.song_parser import SongValuesCommand, SetSongTempoBpm, SetSongTimeSignature 9 | from melody_scripter.song_parser import SetSongTicksPerBeat, SetSongSubTicksPerTick 10 | from melody_scripter.song_parser import TrackValuesCommand, SetTrackInstrument, SetTrackVolume, SetTrackOctave 11 | from melody_scripter.song_parser import SongCommand, Song, find_next_note, Scale, RelativeScale 12 | from melody_scripter.song_parser import Groove, GrooveCommand 13 | 14 | from contextlib import contextmanager 15 | 16 | file_to_parse = FileToParse('name') 17 | 18 | def as_region(string, left_offset = 12, right_offset = 15): 19 | full_text = '*' * left_offset + string + '#' * right_offset 20 | line_to_parse = LineToParse(file_to_parse, 1, full_text) 21 | return LineRegionToParse(line_to_parse, start = left_offset, end = left_offset + len(string)) 22 | 23 | class ParserTestCase(unittest.TestCase): 24 | @contextmanager 25 | def parse_exception(self, message, looking_at): 26 | try: 27 | yield 28 | self.fail('No ParseException') 29 | except ParseException, pe: 30 | self.assertEquals(pe.message, message) 31 | if pe.location is None: 32 | self.fail('ParseException %r has no location given' % pe.message) 33 | self.assertEquals(pe.location.rest_of_line()[0:len(looking_at)], looking_at) 34 | 35 | def assertEqualsDisplaying(self, x, y): 36 | if x != y: 37 | self.fail('Values not equal:\n %r\n %r' % (x, y)) 38 | 39 | class TestLineRegion(ParserTestCase): 40 | 41 | def test_stripped(self): 42 | region = as_region(' hello world ') 43 | self.assertEquals(region.stripped().value, 'hello world') 44 | region = as_region(' hello world') 45 | self.assertEquals(region.stripped().value, 'hello world') 46 | region = as_region('hello world ') 47 | self.assertEquals(region.stripped().value, 'hello world') 48 | 49 | 50 | class TestNoteNames(ParserTestCase): 51 | 52 | def test_note_names(self): 53 | for letter, number in [('c', 0), ('d', 1), ('e', 2), ('f', 3), ('g', 4), ('a', 5), ('b', 6)]: 54 | self.assertEquals(song_parser.scale_number_from_letter(letter), number) 55 | for letter, number in [('C', 0), ('D', 1), ('E', 2), ('F', 3), ('G', 4), ('A', 5), ('B', 6)]: 56 | self.assertEquals(song_parser.scale_number_from_letter(letter), number) 57 | 58 | def test_note_name_strings(self): 59 | for i in range(7): 60 | self.assertEquals(song_parser.scale_number_from_letter(song_parser.NOTE_NAMES_LOWER_CASE[i]), i) 61 | for i in range(7): 62 | self.assertEquals(song_parser.scale_number_from_letter(song_parser.NOTE_NAMES_UPPER_CASE[i]), i) 63 | 64 | class TestScaleNoteWithOctave(ParserTestCase): 65 | 66 | def test_parse_notes(self): 67 | self.assertEquals(scale_note('c0').midi_note, 12) 68 | self.assertEquals(scale_note('e0').note, 2) 69 | self.assertEquals(scale_note('e0').midi_note, 16) 70 | self.assertEquals(scale_note('f1').midi_note, 29) 71 | self.assertEquals(scale_note('f+1').midi_note, 30) 72 | self.assertEquals(scale_note('f+').semitone_offset, 6) 73 | self.assertEquals(scale_note('e').semitone_offset, 4) 74 | self.assertEquals(scale_note('f').semitone_offset, 5) 75 | self.assertEquals(scale_note('b').semitone_offset, 11) 76 | 77 | 78 | class TestFindNextNote(ParserTestCase): 79 | 80 | def _verify_next_note(self, start_note, note, ups, result): 81 | self.assertEquals(find_next_note(scale_note(start_note).midi_note, 82 | scale_note(note).semitone_offset, ups), 83 | scale_note(result).midi_note) 84 | 85 | def test_ups(self): 86 | self._verify_next_note('c3', 'c', 1, 'c4') 87 | self._verify_next_note('c3', 'd', 1, 'd3') 88 | self._verify_next_note('c3', 'a', 1, 'a3') 89 | self._verify_next_note('c3', 'a', 2, 'a4') 90 | self._verify_next_note('f3', 'd', 1, 'd4') 91 | self._verify_next_note('f3', 'g', 1, 'g3') 92 | self._verify_next_note('f3', 'f', 1, 'f4') 93 | 94 | 95 | def test_downs(self): 96 | self._verify_next_note('c3', 'c', -1, 'c2') 97 | self._verify_next_note('c3', 'd', -1, 'd2') 98 | self._verify_next_note('c3', 'a', -1, 'a2') 99 | self._verify_next_note('c3', 'a', -1, 'a2') 100 | self._verify_next_note('f3', 'd', -1, 'd3') 101 | self._verify_next_note('f3', 'g', -1, 'g2') 102 | self._verify_next_note('f3', 'f', -1, 'f2') 103 | 104 | def test_nearest(self): 105 | self._verify_next_note('c3', 'c', 0, 'c3') 106 | self._verify_next_note('c3', 'e', 0, 'e3') 107 | self._verify_next_note('c3', 'f', 0, 'f3') 108 | self._verify_next_note('c3', 'g', 0, 'g2') 109 | self._verify_next_note('f3', 'd', 0, 'd3') 110 | self._verify_next_note('f3', 'b-', 0, 'b-3') 111 | with assert_raises(ParseException) as pe: 112 | self._verify_next_note('f3', 'b', 0, 'b3') 113 | self.assertEquals(pe.exception.message, "Can't decide next nearest note (6 semitones either way)") 114 | 115 | 116 | class TestSongParser(ParserTestCase): 117 | 118 | WORD_REGEX = regex.compile('[a-zA-Z]+') 119 | 120 | def test_script_line(self): 121 | left_offset = 9 122 | region = as_region('This is a line', left_offset = left_offset) 123 | self.assertEquals(region.pos, left_offset) 124 | word = region.parse(self.WORD_REGEX) 125 | self.assertEquals(word.value, 'This') 126 | self.assertEquals(region.pos, left_offset+4) 127 | 128 | def test_match(self): 129 | region = as_region('This is a line') 130 | test_regex = regex.compile(r'(?PThis)?|(?PThat)?') 131 | match = region.match(test_regex) 132 | group_dict = match.groupdict() 133 | self.assertEquals(group_dict['this'], 'This') 134 | self.assertEquals(group_dict['that'], None) 135 | 136 | 137 | class TestNote(ParserTestCase): 138 | 139 | def test_note_equality(self): 140 | note1 = Note(0, 1, 1, (3, 4)) 141 | self.assertEquals(note1, note1) 142 | self.assertEquals(note1, Note(0, 1, 1, (3, 4))) 143 | note1b = Note(0, 1, 1, (3, 4)) 144 | note2 = Note(0, -1, 0, (1, 2)) 145 | self.assertNotEquals(note1, note2) 146 | 147 | def test_note_unparse(self): 148 | note1 = Note(0, 1, 1, (3, 4)) 149 | self.assertEquals(note1.unparse(), 'c+\'3q') 150 | note1 = Note(0, 1, -1, (3, 4)) 151 | self.assertEquals(note1.unparse(), 'c+,3q') 152 | 153 | def test_note_parse(self): 154 | region = as_region('a+\'3q') 155 | note = Note.parse(region) 156 | self.assertEquals(note.source, region) 157 | self.assertEquals(note, Note(5, 1, 1, (3, 4))) 158 | self.assertEquals(note.semitone_offset, 10) 159 | region = as_region('a') 160 | note = Note.parse(region) 161 | self.assertEquals(note.source, region) 162 | self.assertEquals(note, Note(5)) 163 | 164 | region = as_region('a+,3q') 165 | note = Note.parse(region) 166 | self.assertEquals(note, Note(5, 1, -1, (3, 4))) 167 | 168 | def test_parse_rest(self): 169 | region = as_region('r2.') 170 | rest = SongItem.parse(region) 171 | self.assertEquals(rest, Rest((6,2))) 172 | 173 | def test_parse_rest_no_duration(self): 174 | region = as_region('r') 175 | with self.parse_exception('Rest must specify duration', 'r'): 176 | rest = SongItem.parse(region) 177 | 178 | def test_note_parse_exception(self): 179 | with self.parse_exception('Invalid note: "a+\'3qmexico" (extra data "mexico")', 180 | "mexico"): 181 | region = as_region('a+\'3qmexico') 182 | Note.parse(region) 183 | 184 | class TestBarLine(ParserTestCase): 185 | def test_bar_equality(self): 186 | self.assertEquals(BarLine(), BarLine()) 187 | 188 | def test_bar_line_unparse(self): 189 | self.assertEquals(BarLine().unparse(), '|') 190 | 191 | def test_bar_line_parse(self): 192 | region = as_region('|') 193 | bar_line = BarLine.parse(region) 194 | self.assertEquals(bar_line.source, region) 195 | self.assertEquals(bar_line, BarLine()) 196 | 197 | def test_bar_line_parse(self): 198 | with self.parse_exception('Invalid bar line: "|extra" (extra data "extra")', 199 | 'extra'): 200 | region = as_region('|extra') 201 | BarLine.parse(region) 202 | 203 | def test_bar_line_parse_wrong(self): 204 | with self.parse_exception('Invalid bar line: "wrong"', 'wrong'): 205 | region = as_region('wrong') 206 | BarLine.parse(region) 207 | 208 | def test_bar_source_and_unparse(self): 209 | region = as_region('|') 210 | bar_line = BarLine.parse(region) 211 | self.assertEquals(bar_line.source.value, bar_line.unparse()) 212 | 213 | class TestTies(ParserTestCase): 214 | 215 | def test_tie_parse(self): 216 | region = as_region('~') 217 | tie = Tie.parse(region) 218 | self.assertEquals(tie.source, region) 219 | self.assertEquals(tie, Tie()) 220 | 221 | 222 | class TestChord(ParserTestCase): 223 | def test_chord_equality(self): 224 | chord1 = Chord(ScaleNote(0, 1), descriptor = 'maj7') 225 | chord2 = Chord(ScaleNote(0, 1), descriptor = 'maj7') 226 | self.assertEquals(chord1, chord2) 227 | chord3 = Chord(ScaleNote(0, 1), descriptor = 'm7') 228 | self.assertNotEquals(chord1, chord3) 229 | 230 | def test_chord_unparse(self): 231 | self.assertEquals(Chord(ScaleNote(0, 1), descriptor = 'm').unparse(), '[C+m]') 232 | self.assertEquals(Chord(ScaleNote(2), other_notes = [ScaleNote(4), ScaleNote(6)]).unparse(), '[:EGB]') 233 | 234 | def test_chord_parse_and_unparse(self): 235 | region = as_region('[:CE-G]') 236 | parsed_chord = Chord.parse(region) 237 | self.assertEquals(parsed_chord, Chord(ScaleNote(0), other_notes = [ScaleNote(2, -1), ScaleNote(4)])) 238 | self.assertEquals(parsed_chord.source.value, parsed_chord.unparse()) 239 | region = as_region('[Cm7]') 240 | self.assertEquals(Chord.parse(region), Chord(ScaleNote(0), descriptor = 'm7')) 241 | 242 | def test_parse_chord_rest(self): 243 | region = as_region('[]') 244 | parsed_chord = Chord.parse(region) 245 | self.assertEquals(parsed_chord, Chord(None)) 246 | 247 | def test_chord_with_bass(self): 248 | region = as_region('[:DFA]') 249 | parsed_chord = Chord.parse(region) 250 | parsed_chord.resolve(Song()) 251 | self.assertEquals(parsed_chord.bass_midi_note, 14) 252 | 253 | region = as_region('[:DFA/C]') 254 | parsed_chord = Chord.parse(region) 255 | parsed_chord.resolve(Song()) 256 | self.assertEquals(parsed_chord.bass_midi_note, 12) 257 | 258 | def test_chord_descriptor_midi_notes(self): 259 | region = as_region('[B]') 260 | parsed_chord = Chord.parse(region) 261 | parsed_chord.resolve(Song()) 262 | self.assertEquals(parsed_chord.midi_notes, [35, 39, 42]) 263 | 264 | region = as_region('[B-]') 265 | parsed_chord = Chord.parse(region) 266 | parsed_chord.resolve(Song()) 267 | self.assertEquals(parsed_chord.midi_notes, [34, 38, 41]) 268 | 269 | def test_chord_notes_midi_notes(self): 270 | region = as_region('[:CEG]') 271 | parsed_chord = Chord.parse(region) 272 | parsed_chord.resolve(Song()) 273 | self.assertEquals(parsed_chord.midi_notes, [24, 28, 31]) 274 | region = as_region('[:C+EG+]') 275 | parsed_chord = Chord.parse(region) 276 | parsed_chord.resolve(Song()) 277 | self.assertEquals(parsed_chord.midi_notes, [25, 28, 32]) 278 | 279 | 280 | class TestSongItemParser(ParserTestCase): 281 | 282 | def test_parse_chord_song_item(self): 283 | region = as_region('[:CE-G]') 284 | parsed_item = SongItem.parse(region) 285 | self.assertEquals(parsed_item.source, region) 286 | region = as_region('[:CE-G]') 287 | parsed_chord = Chord.parse(region) 288 | self.assertEquals(parsed_item, parsed_chord) 289 | 290 | def test_parse_barline_song_item(self): 291 | region = as_region('|') 292 | parsed_item = SongItem.parse(region) 293 | region = as_region('|') 294 | parsed_barline = BarLine.parse(region) 295 | self.assertEquals(parsed_item, parsed_barline) 296 | 297 | def test_parse_tie_song_item(self): 298 | region = as_region('~') 299 | parsed_item = SongItem.parse(region) 300 | region = as_region('~') 301 | parsed_tie = Tie.parse(region) 302 | self.assertEquals(parsed_item, parsed_tie) 303 | 304 | def test_parse_note_song_item(self): 305 | region = as_region('a+\'3q') 306 | parsed_item = SongItem.parse(region) 307 | region = as_region('a+\'3q') 308 | parsed_note = Note.parse(region) 309 | self.assertEquals(parsed_item, parsed_note) 310 | 311 | def test_parse_tied_notes_song_item(self): 312 | region = as_region('~a+3q') 313 | parsed_item = SongItem.parse(region) 314 | self.assertEquals(parsed_item, Note(5, 1, duration = (3, 4), continued = True)) 315 | region = as_region('a+3q~') 316 | parsed_item = SongItem.parse(region) 317 | self.assertEquals(parsed_item, Note(5, 1, duration = (3, 4), to_continue = True)) 318 | region = as_region('~a+3q~') 319 | parsed_item = SongItem.parse(region) 320 | self.assertEquals(parsed_item, Note(5, 1, duration = (3, 4), to_continue = True, continued = True)) 321 | 322 | def test_invalid_song_item(self): 323 | with self.parse_exception("Invalid song item: 'wrong'", 'wrong'): 324 | SongItem.parse(as_region('wrong')) 325 | 326 | def test_invalid_song_item_starts_like_note(self): 327 | with self.parse_exception('Invalid note: "a\'wrong" (extra data "wrong")', 'wrong'): 328 | SongItem.parse(as_region('a\'wrong')) 329 | 330 | def test_song_item_parse_regions(self): 331 | region = as_region(' [C] c e e c | [G] ') 332 | region.parse(SongItems.parse_regex) 333 | item_regions = region.named_groups('item') 334 | item_region_values = [region.value for region in item_regions] 335 | self.assertEquals(item_region_values, ['[C]', 'c', 'e', 'e', 'c', '|', '[G]']) 336 | 337 | def test_song_items(self): 338 | region = as_region(' [C] c e | [Am] ') 339 | song_items = list(SongItems.parse(region)) 340 | expected_song_items = [Chord(ScaleNote(0), descriptor = ''), 341 | Note(0), Note(2), BarLine(), 342 | Chord(ScaleNote(5), descriptor = 'm')] 343 | self.assertEquals(song_items, expected_song_items) 344 | self.assertEquals(song_items[0].source.value, '[C]') 345 | self.assertEquals(song_items[3].source.value, '|') 346 | 347 | class TestCommandParser(ParserTestCase): 348 | 349 | def test_int_value_parser(self): 350 | region = as_region('23') 351 | track_volume = SetTrackVolume.parse_value(region) 352 | self.assertEquals(track_volume, SetTrackVolume(23)) 353 | with self.parse_exception('Invalid value for volume: 145 - must be an integer from 0 to 127', 354 | '145'): 355 | SetTrackVolume.parse_value(as_region('145')) 356 | with self.parse_exception("Invalid value for volume: '12wrong' - must be an integer from 0 to 127", 357 | '12wrong'): 358 | SetTrackVolume.parse_value(as_region('12wrong')) 359 | with self.parse_exception("Invalid value for volume: 'wrong' - must be an integer from 0 to 127", 360 | 'wrong'): 361 | SetTrackVolume.parse_value(as_region('wrong')) 362 | 363 | 364 | def test_value_setting_parser(self): 365 | region = as_region('tempo_bpm = 80') 366 | volume_setting = SongValuesCommand.parse_value_setting(region) 367 | self.assertEquals(volume_setting.source, region) 368 | self.assertEquals(volume_setting, SetSongTempoBpm(80)) 369 | with self.parse_exception("Invalid value key for song: 'not_tempo_bpm'", 370 | 'not_tempo_bpm'): 371 | SongValuesCommand.parse_value_setting(as_region('not_tempo_bpm = 23')) 372 | with self.parse_exception('Invalid value for tempo_bpm: 23000 - must be an integer from 1 to 1000', 373 | '23000'): 374 | SongValuesCommand.parse_value_setting(as_region('tempo_bpm = 23000')) 375 | 376 | def test_song_values_song_command(self): 377 | command_region = as_region('song: tempo_bpm=80, time_signature = 4/4 ,ticks_per_beat = 12 , subticks_per_tick = 5 ') 378 | 379 | values_command = SongCommand.parse(command_region) 380 | self.assertEquals(values_command.source, command_region) 381 | self.assertEquals(values_command, 382 | SongValuesCommand([SetSongTempoBpm(80), SetSongTimeSignature((4, 4)), 383 | SetSongTicksPerBeat(12), SetSongSubTicksPerTick(5)])) 384 | 385 | def test_trailing_comma(self): 386 | command_region = as_region('song: tempo_bpm=80, time_signature = 4/4, ') 387 | with self.parse_exception("Extra data when parsing: ', '", ', '): 388 | values_command = SongCommand.parse(command_region) 389 | 390 | def test_repeated_commas(self): 391 | command_region = as_region('song: tempo_bpm=80,, time_signature = 4/4, ') 392 | with self.parse_exception("Extra data when parsing: ',, time_signature = 4/4, '", ',, t'): 393 | values_command = SongCommand.parse(command_region) 394 | command_region = as_region('song: tempo_bpm=80, , time_signature = 4/4, ') 395 | with self.parse_exception("Extra data when parsing: ', , time_signature = 4/4, '", ', , t'): 396 | values_command = SongCommand.parse(command_region) 397 | 398 | def test_track_values_command(self): 399 | qualifier_region = as_region('melody') 400 | body_region = as_region('instrument = 73, volume=100, octave=3') 401 | values_command = TrackValuesCommand.parse_command(qualifier_region, body_region) 402 | self.assertEquals(values_command, 403 | TrackValuesCommand('melody', 404 | [SetTrackInstrument(73), SetTrackVolume(100), SetTrackOctave(3)])) 405 | 406 | class TestGroove(ParserTestCase): 407 | 408 | def test_groove(self): 409 | groove = Groove(beats_per_bar = 3, ticks_per_beat = 2, subticks_per_tick = 10, 410 | delays = [0, 3]) 411 | self.assertEquals(groove.get_subticks(tick = 0), 0) 412 | self.assertEquals(groove.get_subticks(tick = 1), 13) 413 | self.assertEquals(groove.get_subticks(tick = 2), 20) 414 | self.assertEquals(groove.get_subticks(tick = 20), 200) 415 | self.assertEquals(groove.get_subticks(tick = 23), 233) 416 | 417 | def test_parse_groove(self): 418 | region = as_region('groove: 0 3') 419 | command = SongCommand.parse(region) 420 | self.assertEquals(command, GrooveCommand(delays = [0, 3])) 421 | 422 | region = as_region('groove: 0 wrong 3') 423 | with self.parse_exception('Invalid groove delay: "wrong"', "wrong 3"): 424 | command = SongCommand.parse(region) 425 | 426 | class TestTimeSignature(ParserTestCase): 427 | 428 | def test_parse_3_4(self): 429 | song_lines = "*song: time_signature = 3/4\n" + "c e | d e f | c" 430 | parse_string = StringToParse('test_string', song_lines) 431 | song = Song.parse(parse_string) 432 | 433 | def test_parse_3_8(self): 434 | song_lines = "*song: time_signature = 3/8\n" + "ch e | dh e f | ch" 435 | parse_string = StringToParse('test_string', song_lines) 436 | song = Song.parse(parse_string) 437 | 438 | def test_parse_2_2_ticks_error(self): 439 | song_lines = "*song: time_signature = 2/2, ticks_per_beat = 1\n" + " e | d e f g | d" 440 | parse_string = StringToParse('test_string', song_lines) 441 | with self.parse_exception('ticks per beat of 1 has to be a multiple of crotchets per beat of 2', 442 | 'ticks_per_beat'): 443 | song = Song.parse(parse_string) 444 | 445 | class TestScaleDeclaration(ParserTestCase): 446 | 447 | def test_relative_scale(self): 448 | relative_scale = RelativeScale('major', [0, 2, 4, 5, 7, 9, 11]) 449 | g_pos = relative_scale.get_position(7) 450 | self.assertEquals(g_pos, 4) 451 | high_f_pos = relative_scale.get_position(17) 452 | self.assertEquals(high_f_pos, 10) 453 | low_a_pos = relative_scale.get_position(-3) 454 | self.assertEquals(low_a_pos, -2) 455 | fsharp_pos = relative_scale.get_position(18) 456 | self.assertEquals(fsharp_pos, None) 457 | 458 | def test_parse_scale(self): 459 | region = as_region('D+3 minor') 460 | scale = Scale.parse(region) 461 | self.assertEquals(scale, Scale(ScaleNote(1, sharps = 1, octave = 3), 462 | RelativeScale('minor', [0, 2, 3, 5, 7, 8, 10]))) 463 | 464 | def test_parse_song_scale(self): 465 | song_lines = "*song: scale = C2 major\n*track.melody: octave = 2\n c e | d e f g | c, d'' " 466 | parse_string = StringToParse('test_string', song_lines) 467 | song = Song.parse(parse_string) 468 | self.assertEquals(song.scale, Scale(ScaleNote(0, octave = 2), 469 | RelativeScale('major', [0, 2, 4, 5, 7, 9, 11]))) 470 | note_items = [item for item in song.items if isinstance(item, Note)] 471 | scale_positions = [song.scale.get_position(note.midi_note) for note in note_items] 472 | self.assertEquals(scale_positions, [0, 2, 1, 2, 3, 4, 0, 8]) 473 | 474 | 475 | 476 | class TestSongParser(ParserTestCase): 477 | 478 | song_lines = """ 479 | *song: tempo_bpm=120, ticks_per_beat=4 480 | *track.chord: octave = 2, instrument=40 481 | *track.bass: octave = 1, volume=90 482 | 483 | [C] c e [:FAC] e ch ch | [G] g r2 484 | """ 485 | def test_parse_song(self): 486 | parse_string = StringToParse('test_string', self.song_lines) 487 | song = Song.parse(parse_string) 488 | self.assertEqualsDisplaying(song, 489 | Song([SongValuesCommand([SetSongTempoBpm(120), 490 | SetSongTicksPerBeat(4)]), 491 | TrackValuesCommand('chord', 492 | [SetTrackOctave(2), SetTrackInstrument(40)]), 493 | TrackValuesCommand('bass', 494 | [SetTrackOctave(1), SetTrackVolume(90)]), 495 | Chord(ScaleNote(0), descriptor = ''), 496 | Note(0, duration = (1, 1)), Note(2, duration = (1, 1)), 497 | Chord(ScaleNote(3), other_notes = [ScaleNote(5), ScaleNote(0)]), 498 | Note(2, duration = (1, 1)), 499 | Note(0, duration = (1, 2)), 500 | Note(0, duration = (1, 2)), 501 | BarLine(), 502 | Chord(ScaleNote(4), descriptor = ''), 503 | Note(4, duration = (1, 1)), 504 | Rest((2, 1))])) 505 | chord_track = song.tracks['chord'] 506 | self.assertEquals(chord_track.instrument, 40) 507 | self.assertEquals(song.tracks['bass'].volume, 90) 508 | self.assertEquals(len(chord_track.items), 3) 509 | self.assertEquals(len(song.tracks['melody'].items), 6) 510 | 511 | self.assertEquals(chord_track.items[0].midi_notes, [36, 40, 43]) 512 | self.assertEquals(chord_track.items[1].bass_midi_note, 29) 513 | self.assertEquals(song.bar_ticks, [0, 16, 32]) 514 | 515 | def test_parse_song_cut(self): 516 | song_lines = """ 517 | *song: tempo_bpm=120, ticks_per_beat=4 518 | | [C] c e 519 | [:FAC] e 520 | ! c | [G] r2 521 | """ 522 | parse_string = StringToParse('test_string', song_lines) 523 | song = Song.parse(parse_string) 524 | self.assertEqualsDisplaying(song, 525 | Song([SongValuesCommand([SetSongTempoBpm(120), 526 | SetSongTicksPerBeat(4)]), 527 | Note(0, duration = (1, 1)), 528 | BarLine(), 529 | Chord(ScaleNote(4), descriptor = ''), 530 | Rest((2, 1))])) 531 | 532 | def test_subticks(self): 533 | song_lines = """ 534 | *song: tempo_bpm=120, ticks_per_beat=4, subticks_per_tick = 5 535 | | [C] c 536 | """ 537 | parse_string = StringToParse('test_string', song_lines) 538 | song = Song.parse(parse_string) 539 | self.assertEqualsDisplaying(song, 540 | Song([SongValuesCommand([SetSongTempoBpm(120), 541 | SetSongTicksPerBeat(4), 542 | SetSongSubTicksPerTick(5)]), 543 | BarLine(), 544 | Chord(ScaleNote(0), descriptor = ''), 545 | Note(0, duration = (1, 1)) 546 | ])) 547 | 548 | def _continuation_song(self, string): 549 | song_lines = "*song: ticks_per_beat=1, time_signature = 4/4\n%s" % string 550 | parse_string = StringToParse('test_string', song_lines) 551 | return Song.parse(parse_string) 552 | 553 | def _verify_durations(self, song_string, expected_note_durations): 554 | song = self._continuation_song(song_string) 555 | melody_track = song.tracks['melody'] 556 | notes_to_verify = [item for item in melody_track.items if not item.continued] 557 | self.assertEquals([note.duration_ticks for note in notes_to_verify], expected_note_durations) 558 | 559 | def test_no_continuations(self): 560 | song = self._verify_durations('| a b b c | d e2 e1 |', [1, 1, 1, 1, 1, 2, 1]) 561 | 562 | def test_invalid_continutations(self): 563 | with self.parse_exception('Tie appears after previous tie', '~ e1 |'): 564 | self._continuation_song('| a b b c | d e2 ~ ~ e1 |') 565 | with self.parse_exception('Note unnecessarily marked to continue after preceding tie', '~e1 |'): 566 | self._continuation_song('| a b b c | d e2 ~ ~e1 |') 567 | with self.parse_exception('Tie appears, but there is no previous note', '~ |'): 568 | self._continuation_song('~ | a b b c | d e2 e1 |') 569 | with self.parse_exception('Note marked as continued, but previous note not marked as to continue', '~b b c'): 570 | self._continuation_song('| a ~b b c | d e2 e1 |') 571 | with self.parse_exception('Note marked as continued, but there is no previous note', '~a b b'): 572 | self._continuation_song('| ~a b b c | d e2 e1 |') 573 | with self.parse_exception('Note not marked as continued, but previous note was marked as to continue', 'b c |'): 574 | self._continuation_song('| b~ ~b~ b c | d e2 e1 |') 575 | with self.parse_exception('Continued note is not the same pitch as previous note', '~b b c'): 576 | self._continuation_song('| a~ ~b b c | d e2 e1 |') 577 | with self.parse_exception('Continued note is not the same pitch as previous note', 'b b c'): 578 | self._continuation_song('| a ~ b b c | d e2 e1 |') 579 | with self.parse_exception('Continued note is not the same pitch as previous note', '~d e2'): 580 | self._continuation_song('| a b b c~ | ~d e2 e1 |') 581 | 582 | def test_valid_continutations(self): 583 | song = self._verify_durations('| b ~ b b c | c e2 e1 |', [2, 1, 1, 1, 2, 1]) 584 | song = self._verify_durations('| b~ ~b b c | c e2 e1 |', [2, 1, 1, 1, 2, 1]) 585 | song = self._verify_durations('| b~ ~b ~ b c | c e2 e1 |', [3, 1, 1, 2, 1]) 586 | song = self._verify_durations('| b~ ~b~ ~b c | c e2 e1 |', [3, 1, 1, 2, 1]) 587 | song = self._verify_durations('| b b b c~ | ~c e2 e1 |', [1, 1, 1, 2, 2, 1]) 588 | song = self._verify_durations('| b ~ b b c | c e2 ~ e1 |', [2, 1, 1, 1, 3]) 589 | song = self._verify_durations('| b ~ b b c | ~ c e2~ ~e1 |', [2, 1, 2, 3]) 590 | 591 | def test_note_too_high(self): 592 | song_lines = """*track.melody: octave = 5\n c c' c' c' | c' d d' e'""" 593 | parse_string = StringToParse('test_string', song_lines) 594 | with self.parse_exception('Note number 134 > 127', "d' e'"): 595 | song = Song.parse(parse_string) 596 | 597 | def test_note_too_low(self): 598 | song_lines = """*track.melody: octave = 4\n c c, c, c, | c, d, e, f, """ 599 | parse_string = StringToParse('test_string', song_lines) 600 | with self.parse_exception('Note number -8 < 0', "e, f, "): 601 | song = Song.parse(parse_string) 602 | 603 | class MidiTrackVisitor(object): 604 | def __init__(self, transpose): 605 | self.transpose = transpose 606 | self.midi_notes = [] 607 | 608 | def add_note(self, note, tick, duration): 609 | self.midi_notes.append(note) 610 | 611 | def add_notes(self, notes, tick, duration): 612 | self.midi_notes.append(notes) 613 | 614 | class TestTransposition(ParserTestCase): 615 | 616 | def test_invalid_transpose_value(self): 617 | song_lines = ("*song: transpose = 310\nc c") 618 | parse_string = StringToParse('test_string', song_lines) 619 | with self.parse_exception('Invalid value for transpose: 310 - must be an integer from -127 to 127', 620 | '310'): 621 | song = Song.parse(parse_string) 622 | 623 | def test_transpose_out_of_range(self): 624 | song_lines = ("*song: transpose = 0\n*track.melody: octave = 8\n c") 625 | parse_string = StringToParse('test_string', song_lines) 626 | song = Song.parse(parse_string) 627 | song_lines = ("*song: transpose = 100\n*track.melody: octave = 8\n c") 628 | parse_string = StringToParse('test_string', song_lines) 629 | song = Song.parse(parse_string) 630 | track_visitor = MidiTrackVisitor(transpose = song.transpose) 631 | note_items = [item for item in song.items if isinstance(item, Note)] 632 | with self.parse_exception('Note number 208 > 127', 'c'): 633 | for note in note_items: 634 | note.visit_midi_track(track_visitor) 635 | 636 | def test_transpose(self): 637 | song_lines = ("*song: transpose = 3\n*track.melody: octave = 2\n*track.chord: octave = 1\n*track.bass:octave = 0\n" + 638 | "c c [C]") 639 | parse_string = StringToParse('test_string', song_lines) 640 | song = Song.parse(parse_string) 641 | note_items = [item for item in song.items if isinstance(item, Note)] 642 | first_note = note_items[0] 643 | self.assertEquals(first_note.midi_note, 36) 644 | track_visitor = MidiTrackVisitor(transpose = song.transpose) 645 | first_note.visit_midi_track(track_visitor) 646 | self.assertEquals(track_visitor.midi_notes[0], 39) 647 | chord_items = [item for item in song.items if isinstance(item, Chord)] 648 | first_chord = chord_items[0] 649 | self.assertEquals(first_chord.midi_notes, [24, 28, 31]) 650 | first_chord.visit_midi_track(track_visitor) 651 | self.assertEquals(track_visitor.midi_notes[1], [27, 31, 34]) 652 | self.assertEquals(first_chord.bass_midi_note, 12) 653 | bass_note = song.tracks['bass'].items[0] 654 | bass_note.visit_midi_track(track_visitor) 655 | self.assertEquals(track_visitor.midi_notes[2], 15) 656 | --------------------------------------------------------------------------------