├── .gitignore ├── MMLX.tmbundle ├── Preferences │ ├── Comments.tmPreferences │ ├── Comments.tmPreferences.cache │ ├── Completions.tmPreferences │ └── Completions.tmPreferences.cache ├── Syntaxes │ ├── MMLX.tmLanguage │ └── MMLX.tmLanguage.cache └── info.plist ├── README.md ├── bin ├── mmlx ├── nesasm ├── nesasm.exe ├── pceas ├── ppmckc ├── ppmckc.exe ├── ppmckc_e └── ppmckc_e.exe ├── files ├── mml │ ├── demo1.mml │ ├── demo1.nsf │ ├── demo2.mml │ ├── demo2.nsf │ ├── demo3.mml │ ├── demo3.nsf │ ├── demo4.mml │ ├── demo4.nsf │ ├── fds.mml │ ├── fds.nsf │ ├── myfirstchiptune.mml │ ├── myfirstchiptune.nsf │ ├── n106.mml │ ├── n106.nsf │ ├── vrc6.mml │ └── vrc6.nsf └── mmlx │ ├── _instruments.mmlx │ ├── demo1.mmlx │ ├── demo2.mmlx │ ├── demo3.mmlx │ ├── demo4.mmlx │ ├── fds.mmlx │ ├── myfirstchiptune.mmlx │ ├── n106.mmlx │ └── vrc6.mmlx ├── mmlxlib ├── __init__.py ├── curve.py ├── instrument.py ├── listener.py ├── logger.py ├── magicmacro.py ├── musicbox.py ├── nes_include │ ├── ppmck.asm │ └── ppmck │ │ ├── dpcm.h │ │ ├── fds.h │ │ ├── fme7.h │ │ ├── freqdata.h │ │ ├── internal.h │ │ ├── mmc5.h │ │ ├── n106.h │ │ ├── sounddrv.h │ │ ├── vrc6.h │ │ └── vrc7.h ├── util.py └── warpwhistle.py ├── setup.py └── tests ├── chips ├── n106.mml ├── n106.mmlx ├── vrc6.mml └── vrc6.mmlx ├── features ├── _import1.mmlx ├── _import2.mmlx ├── _import3.mmlx ├── adsr.mml ├── adsr.mmlx ├── collapse-octave-shifts.mml ├── collapse-octave-shifts.mmlx ├── collapse-spaces.mml ├── collapse-spaces.mmlx ├── comments.mml ├── comments.mmlx ├── curves.mml ├── curves.mmlx ├── import.mml ├── import.mmlx ├── instrument.mml ├── instrument.mmlx ├── magic-macro.mml ├── magic-macro.mmlx ├── repeat.mml ├── repeat.mmlx ├── slide.mml ├── slide.mmlx ├── variables.mml └── variables.mmlx ├── run_tests └── songs ├── demo3.mml ├── demo3.mmlx ├── my-first-chiptune.mml └── my-first-chiptune.mmlx /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /MMLX.tmbundle/Preferences/Comments.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Comments 7 | scope 8 | source.mmlx 9 | settings 10 | 11 | shellVariables 12 | 13 | 14 | name 15 | TM_COMMENT_START 16 | value 17 | // 18 | 19 | 20 | name 21 | TM_COMMENT_START_2 22 | value 23 | ; 24 | 25 | 26 | name 27 | TM_COMMENT_START_3 28 | value 29 | /* 30 | 31 | 32 | name 33 | TM_COMMENT_END_3 34 | value 35 | */ 36 | 37 | 38 | 39 | uuid 40 | 95E61D0B-3C8A-4E4E-8616-2E5E013C461C 41 | 42 | 43 | -------------------------------------------------------------------------------- /MMLX.tmbundle/Preferences/Comments.tmPreferences.cache: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/MMLX.tmbundle/Preferences/Comments.tmPreferences.cache -------------------------------------------------------------------------------- /MMLX.tmbundle/Preferences/Completions.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Completions 7 | scope 8 | source.mmlx 9 | settings 10 | 11 | completions 12 | 13 | adsr 14 | volume 15 | max_volume 16 | vibrato 17 | pitch 18 | timbre 19 | extends 20 | import 21 | ABSOLUTE 22 | NOTES 23 | COUNTER 24 | TRANSPOSE 25 | TITLE 26 | COMPOSER 27 | PROGRAMER 28 | MAKER 29 | OCTAVE 30 | REV 31 | EX 32 | DISKFM 33 | NAMCO106 34 | VRC6 35 | VRC7 36 | MMC5 37 | FME7 38 | BANK 39 | CHANGE 40 | EFFECT 41 | INCLUDE 42 | BANKSWITCH 43 | SETBANK 44 | DPCM 45 | RESTSTOP 46 | 47 | 48 | uuid 49 | 3DA3675F-3AE5-485E-B116-7AD854AE7817 50 | 51 | 52 | -------------------------------------------------------------------------------- /MMLX.tmbundle/Preferences/Completions.tmPreferences.cache: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/MMLX.tmbundle/Preferences/Completions.tmPreferences.cache -------------------------------------------------------------------------------- /MMLX.tmbundle/Syntaxes/MMLX.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fileTypes 6 | 7 | mmlx 8 | 9 | foldingStartMarker 10 | /\*\*|\{\s*$ 11 | foldingStopMarker 12 | \*\*/|^\s*\} 13 | isDisabled 14 | 15 | name 16 | MMLX 17 | patterns 18 | 19 | 20 | begin 21 | " 22 | end 23 | " 24 | name 25 | string.quoted.double 26 | 27 | 28 | match 29 | '(?=\d+) 30 | name 31 | keyword.repeat 32 | 33 | 34 | begin 35 | ' 36 | end 37 | ' 38 | name 39 | string.quoted.single 40 | 41 | 42 | match 43 | @(import|extends)\b 44 | name 45 | storage.mmlx 46 | 47 | 48 | match 49 | (@[a-z-]{3,}[a-zA-Z\-0-9]+)\b 50 | name 51 | storage.mmlx.instrument 52 | 53 | 54 | match 55 | @end\b 56 | name 57 | storage.mmlx.instrument 58 | 59 | 60 | begin 61 | // 62 | end 63 | \n 64 | name 65 | comment.line 66 | 67 | 68 | begin 69 | ; 70 | end 71 | \n 72 | name 73 | comment.line 74 | 75 | 76 | begin 77 | \/\* 78 | end 79 | \*\/ 80 | name 81 | comment.block 82 | 83 | 84 | captures 85 | 86 | 1 87 | 88 | name 89 | storage.mml 90 | 91 | 3 92 | 93 | name 94 | variable 95 | 96 | 97 | match 98 | ^(#(TITLE|COMPOSER|PROGRAMER|MAKER|OCTAVE-REV|GATE-DENOM|INCLUDE|EX-DISKFM|EX-NAMCO106|EX-VRC6|EX-VRC7|EX-MMC5|EX-FME7|BANK-CHANGE|EFFECT-INCLUDE|NO-BANKSWITCH|AUTO-BANKSWITCH|BANK-CHANGE|SETBANK|DPCM-RESTSTOP|PITCH-CORRECTION|X-ABSOLUTE-NOTES|X-TRANSPOSE|X-COUNTER|X-TEMPO|X-SMOOTH))\b(.*)\n 99 | 100 | 101 | match 102 | (SD)[0-9]+ 103 | name 104 | keyword.command 105 | 106 | 107 | match 108 | (SDOF) 109 | name 110 | keyword.command 111 | 112 | 113 | match 114 | (N106|FDS|VRC6|VRC7)-([A-Z]+)\b 115 | name 116 | keyword.mml.voice 117 | 118 | 119 | match 120 | [A-Z]+\b 121 | name 122 | keyword.mml.voice 123 | 124 | 125 | match 126 | \b(o|l|q)[0-9]+\b 127 | name 128 | support.variable 129 | 130 | 131 | match 132 | \b(w|r)[0-9]+\b 133 | name 134 | support.rest 135 | 136 | 137 | begin 138 | (t)([0-9]+) 139 | beginCaptures 140 | 141 | 1 142 | 143 | name 144 | constant 145 | 146 | 2 147 | 148 | name 149 | constant 150 | 151 | 152 | end 153 | \b 154 | 155 | 156 | begin 157 | (\t| {4})(volume|timbre|adsr|max_volume|pitch|arpeggio|vibrato|q|chip|waveform|buffer): {0,}\|? {0,}(-)?([0-9]+)?(N106$|FDS$|VRC6$|VRC7$)? 158 | beginCaptures 159 | 160 | 2 161 | 162 | name 163 | support.function 164 | 165 | 3 166 | 167 | name 168 | keyword.operator 169 | 170 | 4 171 | 172 | name 173 | constant 174 | 175 | 5 176 | 177 | name 178 | constant 179 | 180 | 181 | end 182 | \b 183 | 184 | 185 | begin 186 | \b(?<![a-g]\+)(?<![a-g]\-)(?<!\/)(\d+) 187 | beginCaptures 188 | 189 | 1 190 | 191 | name 192 | constant 193 | 194 | 195 | end 196 | (\]|\b) 197 | 198 | 199 | match 200 | (?<![a-g])[=><\^\.\+\-] 201 | name 202 | keyword.operator 203 | 204 | 205 | captures 206 | 207 | 1 208 | 209 | name 210 | entity.other.attribute-name.instrument 211 | 212 | 213 | match 214 | ^([a-zA-Z0-9\-_]+):( {0,})$ 215 | 216 | 217 | scopeName 218 | source.mmlx 219 | uuid 220 | E90ADC42-15D0-44A8-8664-E9ABF70E453E 221 | 222 | 223 | -------------------------------------------------------------------------------- /MMLX.tmbundle/Syntaxes/MMLX.tmLanguage.cache: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/MMLX.tmbundle/Syntaxes/MMLX.tmLanguage.cache -------------------------------------------------------------------------------- /MMLX.tmbundle/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | MMLX 7 | ordering 8 | 9 | 95E61D0B-3C8A-4E4E-8616-2E5E013C461C 10 | 3DA3675F-3AE5-485E-B116-7AD854AE7817 11 | E90ADC42-15D0-44A8-8664-E9ABF70E453E 12 | 13 | uuid 14 | C16EADEB-EA48-4326-AE0E-7D6755BC4F09 15 | 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | MMLX is a music programming language used to make NES Chiptunes. It extends from Music Macro Language: 4 | http://en.wikipedia.org/wiki/Music_Macro_Language 5 | 6 | It is short for MML eXtended. Everything written in MML is valid in MMLX, but there are additional features. 7 | 8 | If you are familiar with web programming, MMLX is to MML what SASS is to CSS 9 | 10 | For a getting started tutorial check out: 11 | https://github.com/ccampbell/mmlx/wiki/Getting-Started 12 | 13 | For complete documentation: 14 | https://github.com/ccampbell/mmlx/wiki/Documentation 15 | 16 | You can check out some samples in the files/mmlx directory of this repository. Also an MML beginner's guide is available at: 17 | http://nullsleep.com/treasure/mck_guide/ 18 | 19 | ## Dependencies 20 | 21 | **setuptools** or **distribute**: 22 | 23 | curl http://python-distribute.org/distribute_setup.py | python 24 | 25 | **pip**: 26 | 27 | curl https://raw.github.com/pypa/pip/master/contrib/get-pip.py | python 28 | 29 | this may have to be run as root 30 | 31 | ## Quick start (stable release) 32 | pip install mmlx 33 | mmlx --watch path/to/mmlx 34 | 35 | for additional options run 36 | 37 | mmlx --help 38 | 39 | ### Dev version 40 | 41 | If you want to try the latest greatest you can install the dev version 42 | 43 | pip install https://github.com/ccampbell/mmlx/zipball/master 44 | 45 | *NOTE: MMLX has only been tested on Python 2.6.1 using Mac OS X at this time* 46 | 47 | ## Features 48 | * define and use instrument patches 49 | * use ADSR envelopes for creating instruments 50 | * import other MMLX files or instruments into your current file 51 | * use portamento to slide smoothly from one note to the next 52 | * store data such as chords or patterns in variables 53 | * transpose to any key 54 | * target notes directly by octave without having to manually move up and down octaves 55 | * auto generate NSF files on save and open them 56 | * generate separate NSF files for each voice 57 | 58 | ## To-Do 59 | * Throw proper warnings/errors/syntax checks when code is not valid mmlx 60 | * Unit tests 61 | * Automatic DPCM sample conversions from wav files 62 | * Ability to create random instruments 63 | * Ability to use absolute paths to directories 64 | * Add improved support for expansion packs (VRC7, FME7, etc) 65 | * Skip over existing macros defined in your MMLX file when generating new macros from instruments 66 | -------------------------------------------------------------------------------- /bin/mmlx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2012 Craig Campbell 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | import sys, os 17 | 18 | local = False 19 | try: 20 | from mmlxlib.listener import Listener 21 | from mmlxlib.musicbox import MusicBox 22 | from mmlxlib.logger import Logger 23 | except: 24 | local = True 25 | path = os.path.realpath(__file__ + '/../../') 26 | sys.path.append(path) 27 | from mmlxlib.listener import Listener 28 | from mmlxlib.musicbox import MusicBox 29 | from mmlxlib.logger import Logger 30 | 31 | musicbox = MusicBox() 32 | musicbox.play(sys.argv[1:], local) 33 | -------------------------------------------------------------------------------- /bin/nesasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/bin/nesasm -------------------------------------------------------------------------------- /bin/nesasm.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/bin/nesasm.exe -------------------------------------------------------------------------------- /bin/pceas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/bin/pceas -------------------------------------------------------------------------------- /bin/ppmckc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/bin/ppmckc -------------------------------------------------------------------------------- /bin/ppmckc.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/bin/ppmckc.exe -------------------------------------------------------------------------------- /bin/ppmckc_e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/bin/ppmckc_e -------------------------------------------------------------------------------- /bin/ppmckc_e.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/bin/ppmckc_e.exe -------------------------------------------------------------------------------- /files/mml/demo1.mml: -------------------------------------------------------------------------------- 1 | #TITLE Demo 1 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | @0 = { 2 } 5 | @v0 = { 10 } 6 | @v1 = { 9 9 7 5 3 } 7 | @v2 = { 15 10 7 7 7 6 6 5 5 0 } 8 | @EP0 = { -20 | 0 2 0 } 9 | ABCDE t135 10 | A l16 @@0 @v0 q4 o2 [[g+ b > d+ g+]4 < [e g+ b > e]4 < [< b > d+ f+ b]4 [d+ g a+ > c+]4]2 11 | D l16 [@v1 q1 [d]4 @v2 EP0 q4 c EPOF @v1 q1 [d]2 @v2 EP0 q4 c EPOF @v1 q1 [d]4 @v2 EP0 q4 c EPOF @v1 q1 [d]3]8 12 | -------------------------------------------------------------------------------- /files/mml/demo1.nsf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/files/mml/demo1.nsf -------------------------------------------------------------------------------- /files/mml/demo2.mml: -------------------------------------------------------------------------------- 1 | #TITLE Demo 2 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | @0 = { 2 } 5 | @v0 = { 0 2 3 4 5 6 7 8 9 10 } 6 | @v1 = { 11 9 7 5 3 } 7 | @EP0 = { 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 0 } 8 | @EP1 = { 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 0 } 9 | @EP2 = { -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 -3 0 } 10 | @EP3 = { 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 0 } 11 | @EP4 = { 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 0 } 12 | @MP0 = { 8 4 4 } 13 | ABCDE t100 14 | AB @@0 @v0 MP0 15 | A o4 [c2. e2 EP0 e2. EPOF EP1 g1 EPOF]2 > 16 | B o3 [e2. g2 EP2 g2. EPOF EP3 e1 EPOF]2 > 17 | C o4 [c2 EP4 c1 EPOF > q4 < c e g c e g]2 18 | D l4 @v1 q1 [c]24 19 | -------------------------------------------------------------------------------- /files/mml/demo2.nsf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/files/mml/demo2.nsf -------------------------------------------------------------------------------- /files/mml/demo3.mml: -------------------------------------------------------------------------------- 1 | #TITLE Demo 3 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | @20 = { 1 } 5 | @21 = { 2 } 6 | @v20 = { 10 } 7 | @v21 = { 12 } 8 | @EP20 = { 23 23 24 24 24 24 0 } 9 | @EP21 = { 21 21 21 21 21 22 0 } 10 | @EP22 = { -16 -16 -16 -16 -16 -16 0 } 11 | @EN20 = { 0 0 4 0 0 3 0 0 5 0 0 } 12 | @EN21 = { 0 0 3 0 0 4 0 0 5 0 0 } 13 | ABCDE t150 14 | @v1 = { 5 } 15 | A o4 SD0 @vr1 [@@20 @v20 q6 c c @@21 @v21 EN20 q4 c ENOF @@20 @v20 q6 EP20 c EPOF d d @@21 @v21 EN20 q4 d ENOF @@20 @v20 q6 EP21 d EPOF g g @@21 @v21 EN20 q4 g ENOF @@20 @v20 q6 EP22 g EPOF e e @@21 @v21 EN21 q4 e ENOF @@20 @v20 q6 e]2 SDOF 16 | C o4 [c c e8 e8 g8 g8 d d f+8 f+8 a8 a8 g g b8 b8 d8 d8 e e g8 g8 b8 b8]2 17 | -------------------------------------------------------------------------------- /files/mml/demo3.nsf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/files/mml/demo3.nsf -------------------------------------------------------------------------------- /files/mml/demo4.mml: -------------------------------------------------------------------------------- 1 | #TITLE Demo 4 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | @0 = { 1 } 5 | @v0 = { 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 } 6 | @v1 = { 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 0 } 7 | @v2 = { 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 1 1 1 1 1 1 1 0 } 8 | @v3 = { 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 0 } 9 | ABCDE t115 10 | A o4 @@0 @v0 e- @v1 g @v2 b- @v3 > e-2. @v2 c2. < b-2. a-2. @v1 b-2. @v2 d2. 11 | B o3 @@0 @v0 e- @v1 e- @v2 g @v3 e-2. @v2 a-2. g2. f2. @v1 g2. @v2 b-2. 12 | -------------------------------------------------------------------------------- /files/mml/demo4.nsf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/files/mml/demo4.nsf -------------------------------------------------------------------------------- /files/mml/fds.mml: -------------------------------------------------------------------------------- 1 | #TITLE FDS Demo 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | #EX-DISKFM 5 | @v0 = { 63 62 61 59 58 56 55 53 52 50 50 49 47 45 44 42 40 38 37 35 33 32 30 28 26 25 23 21 19 18 16 14 13 11 9 7 6 4 2 0 } 6 | @FM0 = { 00 04 21 38 44 50 54 57 51 50 48 54 54 58 60 63 63 63 62 51 56 56 52 54 55 58 58 54 48 42 25 08 04 08 25 42 48 54 58 58 55 54 52 56 56 51 62 63 63 63 60 58 54 54 48 50 51 57 54 50 44 38 21 04 } 7 | F t80 8 | F o2 @v0 @@0 l16 |: [c e g > c e g]4 << [a > c e a > c e]4 << [f a > c f a > c]4 << [g b > d g b > d]4 :|2 9 | -------------------------------------------------------------------------------- /files/mml/fds.nsf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/files/mml/fds.nsf -------------------------------------------------------------------------------- /files/mml/myfirstchiptune.mml: -------------------------------------------------------------------------------- 1 | #TITLE My First Chiptune 2 | #COMPOSER Your Name 3 | #PROGRAMER Your Name 4 | @0 = { 2 } 5 | @1 = { 0 } 6 | @v0 = { 15 15 14 14 13 13 12 12 11 10 10 10 10 9 9 9 8 8 8 7 7 7 6 6 6 5 5 5 4 4 4 3 3 3 2 2 2 1 1 0 } 7 | @v1 = { 12 12 11 11 10 10 9 9 8 7 7 7 7 7 7 6 6 6 6 5 5 5 5 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 0 } 8 | @v2 = { 10 8 6 4 2 0 } 9 | @MP0 = { 2 4 4 } 10 | ABCD t110 11 | A o4 @@0 @v0 < a+ > c d d+ f g a > c < a+1 > 12 | B o3 @@1 @v1 d d+ f g a a+ > c d+ d1 13 | C o4 MP0 < a+1 > f1 < a+2 > 14 | D @v2 q4 e [f16]4 f8 e f8 f f f8 e8 [f32]8 f 15 | -------------------------------------------------------------------------------- /files/mml/myfirstchiptune.nsf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/files/mml/myfirstchiptune.nsf -------------------------------------------------------------------------------- /files/mml/n106.mml: -------------------------------------------------------------------------------- 1 | #TITLE N106 Demo 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | #EX-NAMCO106 4 5 | #PITCH-CORRECTION 6 | @v0 = { 10 } 7 | @v1 = { 12 } 8 | @v2 = { 6 } 9 | @N0 = { 00, 15 15 15 15 0 0 0 0 } 10 | @N1 = { 01, 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 } 11 | PQR t130 12 | P o2 @v0 @@0 a > c e a e c e g < a > c e a g d < b g f a > c f e < b g+ e a1 13 | Q o1 l8 @v1 @@1 [a > a]4 [c > c]4 << [a > a]4 < [g > g]4 < [f > f]4 < [e > e]4 < a1 14 | R o4 @v0 @@0 @v2 r1 c c c e r1 < b g g b r1^1 c1 15 | -------------------------------------------------------------------------------- /files/mml/n106.nsf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/files/mml/n106.nsf -------------------------------------------------------------------------------- /files/mml/vrc6.mml: -------------------------------------------------------------------------------- 1 | #TITLE VRC6 Demo 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | #EX-VRC6 5 | @0 = { 4 } 6 | @v0 = { 10 8 5 3 0 } 7 | @v1 = { 10 9 8 7 6 5 4 3 2 0 } 8 | @v2 = { 45 } 9 | @v3 = { 40 } 10 | @v4 = { 50 } 11 | @v5 = { 63 } 12 | @EP0 = { -15 -15 | 0 1 0 } 13 | @EP1 = { 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 0 } 14 | @EP2 = { 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 0 } 15 | @EP3 = { -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -3 -3 -3 -3 -3 -3 -3 -3 0 } 16 | @EP4 = { 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 0 } 17 | @EP5 = { 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 0 } 18 | @MP0 = { 2 4 8 } 19 | CDMNO t75 20 | C o4 |: c2. c2. a2. a2. f2. f2. g2. g2. :|2 c2. 21 | D l16 [@v0 q1 d d d @v1 EP0 q8 g EPOF @v0 q1 d d]32 22 | M |: o3 @@0 @v2 c EP1 c2 EPOF g > MP0 MPOF < a. > c. e MP0 EP3 e2 EPOF 23 | M MPOF f EP4 f2 EPOF > f. < MP0 a. < MPOF g. g. b > MP0 :|2 24 | M MPOF c2. 25 | N |: @@0 @v2 @v3 o4 e. c. e. e. c. e. c. a. 26 | N a. a. a. f. b. b. d. b. :|2 27 | N e2. 28 | O |: o2 l16 @v4 q2 [c c c @v5 q4 c @v4 q2 c c]4 < [a a a @v5 q4 a @v4 q2 a a]4 29 | O [f f f @v5 q4 f @v4 q2 f f]4 [g g g @v5 q4 g @v4 q2 g g]4 :|2 30 | O q8 > c2. 31 | -------------------------------------------------------------------------------- /files/mml/vrc6.nsf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/files/mml/vrc6.nsf -------------------------------------------------------------------------------- /files/mmlx/_instruments.mmlx: -------------------------------------------------------------------------------- 1 | /** 2 | * drums 3 | */ 4 | hi-hat: 5 | q: 1 6 | volume: 9 9 7 5 3 7 | 8 | snare: 9 | volume: 15 10 7 7 7 6 6 5 5 0 10 | pitch: -20 | 0 2 0 11 | q: 4 12 | 13 | snare-soft: 14 | @extends "snare" 15 | volume: 8 7 0 16 | 17 | snare-softer: 18 | @extends "snare-soft" 19 | volume: 3 2 0 20 | 21 | /** 22 | * synths 23 | */ 24 | square-short: 25 | volume: 10 26 | timbre: 2 27 | q: 4 28 | -------------------------------------------------------------------------------- /files/mmlx/demo1.mmlx: -------------------------------------------------------------------------------- 1 | #TITLE Demo 1 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | 5 | /* 6 | * MMLX Variables 7 | */ 8 | #X-ABSOLUTE-NOTES 9 | #X-TRANSPOSE -1 10 | 11 | ABCDE t135 12 | 13 | @import "_instruments" 14 | 15 | /* 16 | * Chords 17 | * 18 | * note that these notes are absolute notes (note + octave) 19 | * so a2 here means it is an A in the second octave and not an A that is a half note 20 | * this is triggered using the #X-ABSOLUTE-NOTES option above 21 | */ 22 | a_minor = [a2 c3 e3 a3]4 23 | f_major = [f2 a2 c3 f3]4 24 | c_major = [c2 e2 g2 c3]4 25 | e_major7 = [e2 g+2 b2 d3]4 26 | 27 | verse_beat = @hi-hat [d]4 @snare c @hi-hat [d]2 @snare c @hi-hat [d]4 @snare c @hi-hat [d]3 28 | 29 | A l16 @square-short [a_minor f_major c_major e_major7]2 30 | D l16 [verse_beat]8 31 | -------------------------------------------------------------------------------- /files/mmlx/demo2.mmlx: -------------------------------------------------------------------------------- 1 | #TITLE Demo 2 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | ABCDE t100 5 | 6 | square: 7 | adsr: 10 0 10 0 8 | timbre: 2 9 | vibrato: 8 4 4 10 | 11 | hi-hat: 12 | q: 1 13 | volume: 11 9 7 5 3 14 | 15 | AB @square 16 | A o4 [c2. e2 /4 g2. /4 > c1]2 17 | B o3 [e2. g2 /4 e2. /4 > e1]2 18 | C o4 [c2 /4 > c1 q4 < c e g c e g]2 19 | D l4 @hi-hat [c]24 20 | -------------------------------------------------------------------------------- /files/mmlx/demo3.mmlx: -------------------------------------------------------------------------------- 1 | #TITLE Demo 3 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | ABCDE t150 5 | 6 | // start the counter at 20 since we have already used v1 7 | #X-COUNTER 20 8 | 9 | /** 10 | * instruments 11 | */ 12 | square: 13 | timbre: 1 14 | volume: 10 15 | q: 6 16 | 17 | chord: 18 | timbre: 2 19 | volume: 12 20 | q: 4 21 | 22 | major: 23 | @extends "chord" 24 | arpeggio: 0 0 4 0 0 3 0 0 5 0 0 25 | 26 | minor: 27 | @extends "chord" 28 | arpeggio: 0 0 3 0 0 4 0 0 5 0 0 29 | 30 | // volume for self delay 31 | @v1 = { 5 } 32 | 33 | A o4 SD0 @vr1 [@square c c @major c / @square g d d @major d / @square a g g @major g / @square d e e @minor e @square e]2 SDOF 34 | C o4 [c c e8 e8 g8 g8 d d f+8 f+8 a8 a8 g g b8 b8 d8 d8 e e g8 g8 b8 b8]2 35 | -------------------------------------------------------------------------------- /files/mmlx/demo4.mmlx: -------------------------------------------------------------------------------- 1 | #TITLE Demo 4 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | ABCDE t115 5 | 6 | /** 7 | * instruments 8 | */ 9 | inst1: 10 | adsr: 0 0 4 40 11 | timbre: 1 12 | 13 | inst2: 14 | @extends "inst1" 15 | adsr: 0 0 6 50 16 | 17 | inst3: 18 | @extends "inst1" 19 | adsr: 0 0 8 60 20 | 21 | inst4: 22 | @extends "inst1" 23 | adsr: 0 0 13 75 24 | 25 | 26 | A o4 @inst1 e- @inst2 g @inst3 b- @inst4 > e-2. @inst3 c2. < b-2. a-2. @inst2 b-2. @inst3 d2. 27 | B o3 @inst1 e- @inst2 e- @inst3 g @inst4 e-2. @inst3 a-2. g2. f2. @inst2 g2. @inst3 b-2. 28 | 29 | -------------------------------------------------------------------------------- /files/mmlx/fds.mmlx: -------------------------------------------------------------------------------- 1 | #TITLE FDS Demo 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | #X-TEMPO 80 5 | 6 | /** 7 | * instruments 8 | */ 9 | wurlitzer: 10 | adsr: 0 10 50 30 11 | chip: FDS 12 | max_volume: 63 13 | waveform: 00 04 21 38 44 50 54 57 51 50 48 54 54 58 60 63 63 63 62 51 56 56 52 54 55 58 58 54 48 42 25 08 04 08 25 42 48 54 58 58 55 54 52 56 56 51 62 63 63 63 60 58 54 54 48 50 51 57 54 50 44 38 21 04 14 | 15 | c_major = c e g > c e g 16 | a_minor = a > c e a > c e 17 | f_major = f a > c f a > c 18 | g_major = g b > d g b > d 19 | 20 | FDS-A o2 @wurlitzer l16 |: [c_major]4 << [a_minor]4 << [f_major]4 << [g_major]4 :|2 21 | -------------------------------------------------------------------------------- /files/mmlx/myfirstchiptune.mmlx: -------------------------------------------------------------------------------- 1 | #TITLE My First Chiptune 2 | #COMPOSER Your Name 3 | #PROGRAMER Your Name 4 | 5 | /** 6 | * MMLX Variables 7 | */ 8 | #X-TRANSPOSE -2 9 | 10 | /** 11 | * Instruments 12 | */ 13 | square-wave-piano: 14 | adsr: 0 10 10 30 15 | timbre: 2 16 | 17 | guitar: 18 | max_volume: 12 19 | adsr: 0 10 7 30 20 | timbre: 0 21 | 22 | bass: 23 | vibrato: 2 4 4 24 | 25 | drum: 26 | volume: 10 8 6 4 2 0 27 | q: 4 28 | 29 | ABCD t110 30 | 31 | // pulse wave channel 32 | A o4 @square-wave-piano c d e f g a b > d c1 33 | 34 | // pulse wave channel 35 | B o3 @guitar e f g a b > c d f e1 36 | 37 | // triangle wave channel 38 | C o4 @bass c1 g1 c2 39 | 40 | // noise channel 41 | D @drum e [f16]4 f8 e f8 f f f8 e8 [f32]8 f 42 | -------------------------------------------------------------------------------- /files/mmlx/n106.mmlx: -------------------------------------------------------------------------------- 1 | #TITLE N106 Demo 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | 5 | /** 6 | * You can use the X-TEMPO declaration if you don't know all the voice names 7 | * that will end up in your final document (for example if using expansion chips) 8 | */ 9 | #X-TEMPO 130 10 | 11 | /** 12 | * instruments 13 | */ 14 | square: 15 | volume: 10 16 | chip: N106 17 | waveform: 15 15 15 15 0 0 0 0 18 | 19 | sawtooth: 20 | volume: 12 21 | chip: N106 22 | waveform: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 23 | buffer: 1 24 | 25 | soft: 26 | volume: 6 27 | 28 | N106-A o2 @square a > c e a e c e g < a > c e a g d < b g f a > c f e < b g+ e a1 29 | N106-B o1 l8 @sawtooth [a > a]4 [c > c]4 << [a > a]4 < [g > g]4 < [f > f]4 < [e > e]4 < a1 30 | N106-C o4 @square +@soft r1 c c c e r1 < b g g b r1^1 c1 31 | -------------------------------------------------------------------------------- /files/mmlx/vrc6.mmlx: -------------------------------------------------------------------------------- 1 | #TITLE VRC6 Demo 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | #X-TEMPO 75 5 | 6 | square: 7 | volume: 45 8 | chip: VRC6 9 | timbre: 4 10 | 11 | soft: 12 | volume: 40 13 | 14 | sawtooth: 15 | volume: 50 16 | chip: VRC6 17 | q: 2 18 | 19 | long: 20 | volume: 63 21 | q: 4 22 | 23 | vibrato: 24 | vibrato: 2 4 8 25 | 26 | hi-hat: 27 | adsr: 0 0 10 5 28 | q: 1 29 | 30 | snare: 31 | adsr: 0 0 10 10 32 | pitch: -15 -15 | 0 1 0 33 | q: 8 34 | 35 | C o4 |: c2. c2. a2. a2. f2. f2. g2. g2. :|2 c2. 36 | D l16 [@hi-hat d d d @snare g @hi-hat d d]32 37 | 38 | VRC6-A |: o3 @square c /8 e2 g /8 > +@vibrato c2 @square < a. > c. e /8 +@vibrato c2 39 | VRC6-A @square f /8 > c2 f. < +@vibrato a. < @square g. g. b /8 > +@vibrato d2 :|2 40 | VRC6-A @square c2. 41 | 42 | VRC6-B |: @square +@soft o4 e. c. e. e. c. e. c. a. 43 | VRC6-B a. a. a. f. b. b. d. b. :|2 44 | VRC6-B e2. 45 | 46 | VRC6-C |: o2 l16 @sawtooth [c c c +@long c @sawtooth c c]4 < [a a a +@long a @sawtooth a a]4 47 | VRC6-C [f f f +@long f @sawtooth f f]4 [g g g +@long g @sawtooth g g]4 :|2 48 | VRC6-C q8 > c2. 49 | -------------------------------------------------------------------------------- /mmlxlib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/mmlxlib/__init__.py -------------------------------------------------------------------------------- /mmlxlib/curve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2012 Craig Campbell 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # TERMS OF USE - EASING EQUATIONS 18 | # 19 | # Open source under the BSD License. 20 | # 21 | # Copyright 2001 Robert Penner 22 | # All rights reserved. 23 | # 24 | # Redistribution and use in source and binary forms, with or without modification, 25 | # are permitted provided that the following conditions are met: 26 | # 27 | # Redistributions of source code must retain the above copyright notice, this list of 28 | # conditions and the following disclaimer. 29 | # 30 | # Redistributions in binary form must reproduce the above copyright notice, this list 31 | # of conditions and the following disclaimer in the documentation and/or other materials 32 | # provided with the distribution. 33 | # 34 | # Neither the name of the author nor the names of contributors may be used to endorse 35 | # or promote products derived from this software without specific prior written permission. 36 | # 37 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 38 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 39 | # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 40 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 41 | # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 42 | # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 43 | # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 44 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 45 | # OF THE POSSIBILITY OF SUCH DAMAGE. 46 | 47 | # .::::. 48 | # .::::::::. 49 | # ::::::::::: 50 | # ':::::::::::.. 51 | # :::::::::::::::' 52 | # ':::::::::::. 53 | # .::::::::::::::' 54 | # .:::::::::::... 55 | # ::::::::::::::'' 56 | # .:::. '::::::::'':::: 57 | # .::::::::. ':::::' ':::: 58 | # .::::':::::::. ::::: '::::. 59 | # .:::::' ':::::::::. ::::: ':::. 60 | # .:::::' ':::::::::.::::: '::. 61 | # .::::'' ':::::::::::::: '::. 62 | # .::'' ':::::::::::: :::... 63 | # ..:::: ':::::::::' .:' '''' 64 | # ..''''':' ':::::.' 65 | 66 | 67 | class Curve(object): 68 | def __init__(self, begin, end, duration): 69 | self.begin = begin 70 | self.end = end 71 | self.change = end - begin 72 | self.duration = duration 73 | 74 | # ============================================================== 75 | # EASE IN QUAD 76 | # ============================================================== 77 | # * 78 | # * 79 | # * 80 | # * 81 | # * 82 | # * 83 | # * 84 | # * * 85 | # * 86 | # * * 87 | # * 88 | # * * 89 | # * * 90 | # * * * 91 | # * * * 92 | # * * * * * * * * 93 | # ============================================================== 94 | def easeInQuad(self, t, b, c, d): 95 | t = t / d 96 | return c * t * t + b 97 | 98 | # ============================================================== 99 | # EASE OUT QUAD 100 | # ============================================================== 101 | # * 102 | # * * * * * * * 103 | # * * * 104 | # * * * 105 | # * * 106 | # * * 107 | # * 108 | # * * 109 | # * 110 | # * * 111 | # * 112 | # * 113 | # * 114 | # * 115 | # * 116 | # * * 117 | # ============================================================== 118 | def easeOutQuad(self, t, b, c, d): 119 | t = t / d 120 | return -c * t * (t - 2) + b 121 | 122 | # ============================================================== 123 | # EASE IN OUT QUAD 124 | # ============================================================== 125 | # * 126 | # * * * * * 127 | # * * 128 | # * * 129 | # * 130 | # * * 131 | # * 132 | # * 133 | # * 134 | # * 135 | # * 136 | # * * 137 | # * 138 | # * * 139 | # * * 140 | # * * * * * * 141 | # ============================================================== 142 | def easeInOutQuad(self, t, b, c, d): 143 | t = t / (d / 2) 144 | if t < 1: 145 | return c / 2 * t * t + b 146 | 147 | t -= 1 148 | return -c / 2 * (t * (t - 2) - 1) + b 149 | 150 | # ============================================================== 151 | # EASE IN CUBIC 152 | # ============================================================== 153 | # * 154 | # 155 | # * 156 | # * 157 | # 158 | # * 159 | # * 160 | # * 161 | # * 162 | # * 163 | # * * 164 | # * 165 | # * * 166 | # * * 167 | # * * * 168 | # * * * * * * * * * * * * * 169 | # ============================================================== 170 | def easeInCubic(self, t, b, c, d): 171 | t = t / d 172 | return c * t * t * t + b 173 | 174 | # ============================================================== 175 | # EASE OUT CUBIC 176 | # ============================================================== 177 | # * 178 | # * * * * * * * * * * * * 179 | # * * * 180 | # * * 181 | # * * 182 | # * 183 | # * * 184 | # * 185 | # * 186 | # * 187 | # * 188 | # * 189 | # 190 | # * 191 | # * 192 | # * 193 | # ============================================================== 194 | def easeOutCubic(self, t, b, c, d): 195 | t = t / d - 1 196 | return c * (t * t * t + 1) + b 197 | 198 | # ============================================================== 199 | # EASE IN OUT CUBIC 200 | # ============================================================== 201 | # * 202 | # * * * * * * * 203 | # * * 204 | # * * 205 | # * 206 | # * 207 | # 208 | # * 209 | # * 210 | # * 211 | # 212 | # * 213 | # * 214 | # * * 215 | # * * 216 | # * * * * * * * * 217 | # ============================================================== 218 | def easeInOutCubic(self, t, b, c, d): 219 | t = t / (d / 2) 220 | if t < 1: 221 | return c / 2 * t * t * t + b 222 | 223 | t -= 2 224 | return c / 2 * (t * t * t + 2) + b 225 | 226 | # ============================================================== 227 | # EASE IN QUART 228 | # ============================================================== 229 | # * 230 | # 231 | # * 232 | # 233 | # * 234 | # 235 | # * 236 | # * 237 | # * 238 | # * 239 | # * 240 | # * 241 | # * 242 | # * * 243 | # * * * 244 | # * * * * * * * * * * * * * * * * 245 | # ============================================================== 246 | def easeInQuart(self, t, b, c, d): 247 | t = t / d 248 | return c * t * t * t * t + b 249 | 250 | # ============================================================== 251 | # EASE OUT QUART 252 | # ============================================================== 253 | # * 254 | # * * * * * * * * * * * * * * * 255 | # * * * 256 | # * * 257 | # * 258 | # * 259 | # * 260 | # * 261 | # * 262 | # * 263 | # * 264 | # 265 | # * 266 | # 267 | # * 268 | # * 269 | # ============================================================== 270 | def easeOutQuart(self, t, b, c, d): 271 | t = t / d - 1 272 | return -c * (t * t * t * t - 1) + b 273 | 274 | # ============================================================== 275 | # EASE IN OUT QUART 276 | # ============================================================== 277 | # * 278 | # * * * * * * * * * 279 | # * 280 | # * 281 | # * 282 | # * 283 | # * 284 | # 285 | # * 286 | # 287 | # * 288 | # * 289 | # * 290 | # * 291 | # * 292 | # * * * * * * * * * * 293 | # ============================================================== 294 | def easeInOutQuart(self, t, b, c, d): 295 | t = t / (d / 2) 296 | if t < 1: 297 | return c / 2 * t * t * t * t + b 298 | 299 | t -= 2 300 | return -c / 2 * (t * t * t * t - 2) + b 301 | 302 | def render(self, curve_type): 303 | time = 0 304 | positions = [] 305 | while time <= self.duration: 306 | pos = getattr(self, curve_type)(time, self.begin, self.change, self.duration) 307 | positions.append(str(int(pos))) 308 | time += 1 309 | 310 | return ' '.join(positions) 311 | -------------------------------------------------------------------------------- /mmlxlib/instrument.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2012 Craig Campbell 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | from util import Util 17 | from magicmacro import MagicMacro 18 | import math 19 | import re 20 | 21 | 22 | class Instrument(object): 23 | 24 | def __init__(self, data): 25 | valid_chips = ['N106', 'FDS', 'VRC6'] 26 | 27 | for key in data: 28 | if key == 'chip' and data[key] not in valid_chips: 29 | raise Exception('value for chip is not valid: ' + data[key]) 30 | 31 | setattr(self, key, data[key]) 32 | 33 | if hasattr(self, 'adsr'): 34 | self.volume = self.getVolumeFromADSR(self.adsr) 35 | 36 | if hasattr(self, 'volume'): 37 | self.volume = self.magicMacro(self.volume) 38 | 39 | if self.getChip() == 'N106': 40 | Instrument.N106_buffers[self.waveform] = int(self.buffer) if hasattr(self, 'buffer') else None 41 | 42 | # attack - time taken for amplitude to rise from 0 to max (15) 43 | # decay - time taken for amplitude to drop to sustain level 44 | # sustain - amplitude at which the note is held 45 | # release - time taken for amplitude to drop from sustain level to 0 46 | # 47 | # note: 48 | # if decay is 0, max amplitude is the sustain value 49 | # 50 | def getVolumeFromADSR(self, adsr): 51 | bits = adsr.split(' ') 52 | attack = bits[0] 53 | decay = bits[1] 54 | sustain = bits[2] 55 | release = bits[3] 56 | 57 | max_volume = sustain if int(decay) == 0 else 15 58 | 59 | if hasattr(self, "max_volume"): 60 | max_volume = int(self.max_volume) 61 | 62 | if attack == '0' and decay == '0' and sustain == '0' and release != '0': 63 | sustain = 15 64 | 65 | volume = '' 66 | if attack != '0': 67 | volume += str(max_volume) + ' ' if attack == '0' else ' '.join(self.divideIntoSteps(0, max_volume, attack)) + ' ' 68 | 69 | if sustain != '0': 70 | volume += ' '.join(self.divideIntoSteps(max_volume, sustain, decay)) + ' ' 71 | 72 | if release != '0': 73 | volume += ' '.join(self.divideIntoSteps(sustain, 0, release)) + ' ' 74 | 75 | if volume.strip() == '': 76 | volume = '0' 77 | 78 | return volume 79 | 80 | def divideIntoSteps(self, min, max, steps): 81 | min = int(min) 82 | max = int(max) 83 | steps = int(steps) 84 | 85 | # print 'MIN', min 86 | # print 'MAX', max 87 | # print 'STEPS', steps 88 | 89 | if steps == 1: 90 | return [str(min), str(max)] 91 | 92 | # figure out the equation of a line to match these coordinates 93 | # coordinates are (0, min) and (steps - 1, max) 94 | slope = float(max - min) / float(steps - 1) 95 | equation = lambda x: slope * x + min 96 | 97 | values = [] 98 | for x in range(0, steps): 99 | value = int(math.ceil(equation(x))) 100 | values.append(str(value)) 101 | 102 | # print values 103 | return values 104 | 105 | def findBracketObject(self, macro): 106 | open_bracket = 0 107 | pos = 0 108 | started = False 109 | wait_for_end = False 110 | start_pos = 0 111 | 112 | for char in macro: 113 | if wait_for_end and char == ' ': 114 | break 115 | 116 | if char == '[': 117 | if not started: 118 | start_pos = pos 119 | 120 | started = True 121 | open_bracket += 1 122 | 123 | if char == ']': 124 | open_bracket -= 1 125 | 126 | if started and open_bracket == 0: 127 | wait_for_end = True 128 | 129 | pos += 1 130 | 131 | return re.match(r'(\[(.*)\]((\.[a-zA-Z]{1}.*?\))+))', macro[start_pos:pos]) 132 | 133 | def magicMacroObjects(self, macro): 134 | # bracket objects 135 | match = self.findBracketObject(macro) 136 | original = True 137 | 138 | if not match: 139 | original = False 140 | # no bracket 141 | match = re.match(r'((.*?)((\.[a-zA-Z]{1}.*?\))+))', macro) 142 | 143 | if match: 144 | # print 'MACRO',macro 145 | # print 'ORIGINAL',original 146 | # print 'GROUP1',match.group(1) 147 | # print 'GROUP2',match.group(2) 148 | # print 'GROUP3',match.group(3) 149 | 150 | # print 'GROUP4',match.group(4) 151 | magic = MagicMacro(self.magicMacroObjects(match.group(2).split(' ').pop() if not original else match.group(2))) 152 | # magic = MagicMacro(macro.replace(match.group(1), self.magicMacroObjects(match.group(2)), 1)) 153 | 154 | methods = match.group(3)[1:-1].split(').') 155 | for method in methods: 156 | getattr(magic, method.split('(')[0])(*method.split('(')[1].split(',')) 157 | 158 | macro = macro.replace(match.group(1).split(' ').pop() if not original else match.group(1), str(magic), 1) 159 | 160 | return self.magicMacroObjects(macro) 161 | 162 | # print macro 163 | return str(MagicMacro(macro)) 164 | 165 | def magicMacro(self, macro): 166 | return self.magicMacroObjects(macro).strip() 167 | 168 | def getChip(self): 169 | if hasattr(self, 'chip'): 170 | return self.chip 171 | 172 | return None 173 | 174 | @staticmethod 175 | def reset(counter=0): 176 | counter = int(counter) 177 | 178 | Instrument.counters = { 179 | 'timbre': counter, 180 | 'volume': counter, 181 | 'pitch': counter, 182 | 'arpeggio': counter, 183 | 'vibrato': counter, 184 | 'N106': counter, 185 | 'FDS': counter 186 | } 187 | 188 | Instrument.timbres = {} 189 | Instrument.volumes = {} 190 | Instrument.pitches = {} 191 | Instrument.arpeggios = {} 192 | Instrument.vibratos = {} 193 | Instrument.N106 = {} 194 | Instrument.N106_buffers = {} 195 | Instrument.FDS = {} 196 | 197 | def hasParent(self): 198 | return hasattr(self, 'extends') and self.extends is not None 199 | 200 | def inherit(self, instrument): 201 | dictionary = instrument.__dict__ 202 | for key in dictionary: 203 | if key == "extends" or not hasattr(self, key): 204 | setattr(self, key, dictionary[key]) 205 | 206 | if not "extends" in dictionary: 207 | delattr(self, "extends") 208 | 209 | def getParent(self): 210 | return self.extends 211 | 212 | def getCountFor(self, macro): 213 | i = Instrument.counters[macro] 214 | Instrument.counters[macro] += 1 215 | return i 216 | 217 | def getVolumeMacro(self): 218 | i = Instrument.volumes[self.volume] if self.volume in Instrument.volumes else self.getCountFor('volume') 219 | Instrument.volumes[self.volume] = i 220 | return '@v' + str(i) 221 | 222 | def getPitchMacro(self): 223 | i = Instrument.pitches[self.pitch] if self.pitch in Instrument.pitches else self.getCountFor('pitch') 224 | Instrument.pitches[self.pitch] = i 225 | return 'EP' + str(i) 226 | 227 | def getArpeggioMacro(self): 228 | i = Instrument.arpeggios[self.arpeggio] if self.arpeggio in Instrument.arpeggios else self.getCountFor('arpeggio') 229 | Instrument.arpeggios[self.arpeggio] = i 230 | return 'EN' + str(i) 231 | 232 | def getTimbreMacro(self): 233 | i = Instrument.timbres[self.timbre] if self.timbre in Instrument.timbres else self.getCountFor('timbre') 234 | Instrument.timbres[self.timbre] = i 235 | return '@@' + str(i) 236 | 237 | def getVibratoMacro(self): 238 | i = Instrument.vibratos[self.vibrato] if self.vibrato in Instrument.vibratos else self.getCountFor('vibrato') 239 | Instrument.vibratos[self.vibrato] = i 240 | return 'MP' + str(i) 241 | 242 | def getN106Macro(self): 243 | i = Instrument.N106[self.waveform] if self.waveform in Instrument.N106 else self.getCountFor('N106') 244 | Instrument.N106[self.waveform] = i 245 | return '@@' + str(i) 246 | 247 | def getFDSMacro(self): 248 | i = Instrument.FDS[self.waveform] if self.waveform in Instrument.FDS else self.getCountFor('FDS') 249 | Instrument.FDS[self.waveform] = i 250 | return '@@' + str(i) 251 | 252 | @staticmethod 253 | def hasBeenUsed(): 254 | macros = ["timbres", "volumes", "pitches", "arpeggios", "vibratos", "N106", "FDS"] 255 | for macro in macros: 256 | if len(getattr(Instrument, macro)) > 0: 257 | return True 258 | 259 | return False 260 | 261 | @staticmethod 262 | def render(): 263 | macros = '' 264 | 265 | # render timbres 266 | for timbre in Util.sortDictionary(Instrument.timbres): 267 | macros += '@' + str(timbre[1]) + ' = { ' + timbre[0] + ' }\n' 268 | 269 | # render volumes 270 | for volume in Util.sortDictionary(Instrument.volumes): 271 | macros += '@v' + str(volume[1]) + ' = { ' + volume[0] + ' }\n' 272 | 273 | # render pitches 274 | for pitch in Util.sortDictionary(Instrument.pitches): 275 | macros += '@EP' + str(pitch[1]) + ' = { ' + pitch[0] + ' }\n' 276 | 277 | # render arpeggios 278 | for arpeggio in Util.sortDictionary(Instrument.arpeggios): 279 | macros += '@EN' + str(arpeggio[1]) + ' = { ' + arpeggio[0] + ' }\n' 280 | 281 | # render vibratos 282 | for vibrato in Util.sortDictionary(Instrument.vibratos): 283 | macros += '@MP' + str(vibrato[1]) + ' = { ' + vibrato[0] + ' }\n' 284 | 285 | # render N106 286 | for macro in Util.sortDictionary(Instrument.N106): 287 | waveform = Instrument.validateN106(macro[0]) 288 | macros += '@N' + str(macro[1]) + ' = { ' + Instrument.getN106Buffer(waveform) + ', ' + waveform + ' }\n' 289 | 290 | # render FDS 291 | for macro in Util.sortDictionary(Instrument.FDS): 292 | macros += '@FM' + str(macro[1]) + ' = { ' + Instrument.validateFds(macro[0]) + ' }\n' 293 | 294 | return macros 295 | 296 | @staticmethod 297 | def validateN106(macro): 298 | bits = macro.strip().split(' ') 299 | if len(bits) % 4 != 0: 300 | raise Exception('N106 waveform samples have to be a multiple of 4') 301 | 302 | for bit in bits: 303 | bit = int(bit.replace('$', ''), 16) if bit.startswith('$') else int(bit) 304 | if bit < 0: 305 | raise Exception('N106 waveform parameter cannot be less than 0') 306 | 307 | if bit > 15: 308 | raise Exception('N106 waveform parameter cannot be greater than 15') 309 | 310 | return macro 311 | 312 | @staticmethod 313 | def validateFds(macro): 314 | bits = macro.strip().split(' ') 315 | if len(bits) != 64: 316 | raise Exception('FDS waveform must have exactly 64 parameters') 317 | 318 | for bit in bits: 319 | bit = int(bit) 320 | if bit < 0: 321 | raise Exception('FDS waveform parameter cannot be less than 0') 322 | 323 | if bit > 63: 324 | raise Exception('FDS waveform parameter cannot be greater than 63') 325 | 326 | return macro 327 | 328 | @staticmethod 329 | def maxBufferFromSampleLength(sample_length): 330 | map = { 331 | 32: 3, 332 | 28: 3, 333 | 24: 4, 334 | 20: 5, 335 | 16: 7, 336 | 12: 9, 337 | 8: 13, 338 | 4: 32 339 | } 340 | 341 | return map[sample_length] 342 | 343 | @staticmethod 344 | def getBufferForWaveform(waveform): 345 | return Instrument.N106_buffers[waveform] 346 | 347 | @staticmethod 348 | def getN106Buffer(waveform): 349 | waveform = waveform.strip() 350 | bits = waveform.split(' ') 351 | 352 | max_allowed_buffer = Instrument.maxBufferFromSampleLength(len(bits)) 353 | buffer = Instrument.getBufferForWaveform(waveform) 354 | 355 | if buffer is None: 356 | return '00' 357 | 358 | if buffer > max_allowed_buffer: 359 | raise Exception('buffer value cannot be greater than: ' + str(max_allowed_buffer) + ' for ' + str(len(bits)) + ' samples') 360 | 361 | if buffer < 10: 362 | buffer = '0' + str(buffer) 363 | 364 | return str(buffer) 365 | 366 | def start(self, whistle): 367 | start = '' 368 | if hasattr(self, 'timbre'): 369 | last_timbre = whistle.getDataForVoice(whistle.current_voices[0], 'timbre') 370 | new_timbre = self.getTimbreMacro() 371 | 372 | if new_timbre != last_timbre: 373 | whistle.setDataForVoices(whistle.current_voices, 'timbre', new_timbre) 374 | start += new_timbre + ' ' 375 | 376 | if hasattr(self, 'volume'): 377 | last_volume = whistle.getDataForVoice(whistle.current_voices[0], 'volume') 378 | new_volume = self.getVolumeMacro() 379 | 380 | if new_volume != last_volume: 381 | whistle.setDataForVoices(whistle.current_voices, 'volume', new_volume) 382 | start += new_volume + ' ' 383 | 384 | if hasattr(self, 'pitch'): 385 | last_pitch = whistle.getDataForVoice(whistle.current_voices[0], 'pitch') 386 | new_pitch = self.getPitchMacro() 387 | 388 | if new_pitch != last_pitch: 389 | whistle.setDataForVoices(whistle.current_voices, 'pitch', new_pitch) 390 | start += new_pitch + ' ' 391 | 392 | if hasattr(self, 'arpeggio'): 393 | last_arpeggio = whistle.getDataForVoice(whistle.current_voices[0], 'arpeggio') 394 | new_arpeggio = self.getArpeggioMacro() 395 | 396 | if new_arpeggio != last_arpeggio: 397 | whistle.setDataForVoices(whistle.current_voices, 'arpeggio', new_arpeggio) 398 | start += new_arpeggio + ' ' 399 | 400 | if hasattr(self, 'vibrato'): 401 | last_vibrato = whistle.getDataForVoice(whistle.current_voices[0], 'vibrato') 402 | new_vibrato = self.getVibratoMacro() 403 | 404 | if new_vibrato != last_vibrato: 405 | whistle.setDataForVoices(whistle.current_voices, 'vibrato', new_vibrato) 406 | start += new_vibrato + ' ' 407 | 408 | if hasattr(self, 'q'): 409 | last_q = whistle.getDataForVoice(whistle.current_voices[0], 'q') 410 | new_q = 'q' + self.q 411 | 412 | if new_q != last_q: 413 | whistle.setDataForVoices(whistle.current_voices, 'q', new_q) 414 | start += new_q + ' ' 415 | 416 | if hasattr(self, 'waveform') and self.getChip() == 'N106': 417 | last_n106 = whistle.getDataForVoice(whistle.current_voices[0], 'timbre') 418 | new_n106 = self.getN106Macro() 419 | 420 | if new_n106 != last_n106: 421 | whistle.setDataForVoices(whistle.current_voices, 'timbre', new_n106) 422 | start += new_n106 + ' ' 423 | 424 | if hasattr(self, 'waveform') and self.getChip() == 'FDS': 425 | last_fds = whistle.getDataForVoice(whistle.current_voices[0], 'timbre') 426 | new_fds = self.getFDSMacro() 427 | 428 | if new_fds != last_fds: 429 | whistle.setDataForVoices(whistle.current_voices, 'timbre', new_fds) 430 | start += new_fds + ' ' 431 | 432 | return start 433 | 434 | def end(self, whistle): 435 | end = '' 436 | if hasattr(self, 'pitch'): 437 | whistle.setDataForVoices(whistle.current_voices, 'pitch', None) 438 | end += 'EPOF ' 439 | 440 | if hasattr(self, 'arpeggio'): 441 | whistle.setDataForVoices(whistle.current_voices, 'arpeggio', None) 442 | end += 'ENOF ' 443 | 444 | if hasattr(self, 'vibrato'): 445 | whistle.setDataForVoices(whistle.current_voices, 'vibrato', None) 446 | end += 'MPOF ' 447 | 448 | return end 449 | -------------------------------------------------------------------------------- /mmlxlib/listener.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2012 Craig Campbell 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | import os 17 | import glob 18 | import time 19 | import sys 20 | 21 | 22 | class Listener(object): 23 | 24 | def __init__(self, logger=None): 25 | self.watching = False 26 | self.callback = None 27 | self.first_run = True 28 | self.file_list = {} 29 | self.logger = logger 30 | 31 | def getFilesFromDir(self, path, extension=""): 32 | path = path + "/*" 33 | 34 | if not extension == "": 35 | path = path + "." + extension.lstrip(".") 36 | 37 | return glob.glob(path) 38 | 39 | def onChange(self, callback): 40 | self.callback = callback 41 | 42 | def process(self, start, end, is_dir=None): 43 | try: 44 | if is_dir is None: 45 | is_dir = os.path.isdir(start) 46 | 47 | if is_dir: 48 | files = self.getFilesFromDir(start, "mmlx") 49 | output_file = None 50 | else: 51 | files = [start] 52 | output_file = end 53 | 54 | initial_output_file = output_file 55 | 56 | for file in files: 57 | output_file = initial_output_file 58 | 59 | filename = os.path.basename(file) 60 | if filename.startswith('_'): 61 | continue 62 | 63 | last_changed = os.stat(file).st_mtime 64 | output_file = output_file if output_file is not None else os.path.join(end, filename.replace(".mmlx", ".mml")) 65 | 66 | if not file in self.file_list: 67 | self.file_list[file] = last_changed 68 | self.callback(file, output_file) 69 | continue 70 | 71 | if last_changed != self.file_list[file]: 72 | self.logger.log(self.logger.color("detected change to: ", self.logger.GRAY) + self.logger.color(file, self.logger.UNDERLINE)) 73 | self.file_list[file] = last_changed 74 | self.callback(file, output_file, True) 75 | 76 | if self.first_run: 77 | self.logger.log('') 78 | self.first_run = False 79 | 80 | except Exception: 81 | self.logger.log(self.logger.color('Sorry, an error occured:\n', self.logger.RED)) 82 | import traceback 83 | lines = traceback.format_exc().splitlines() 84 | self.logger.log(self.logger.color(lines.pop(), self.logger.RED) + '\n') 85 | self.logger.log('\n'.join(lines)) 86 | self.logger.log('') 87 | 88 | # continue watching 89 | if self.watching: 90 | self.watch(start, end) 91 | return 92 | 93 | sys.exit(1) 94 | 95 | def watch(self, start, end): 96 | try: 97 | self.watching = True 98 | is_dir = os.path.isdir(start) 99 | while 1: 100 | self.process(start, end, is_dir) 101 | time.sleep(.5) 102 | except KeyboardInterrupt: 103 | phrases = [ 104 | 'Sayonara!', 105 | 'Goodbye! Have a nice day!', 106 | 'Come back soon!', 107 | 'Sad to see you leaving already!', 108 | 'Hasta la vista, baby' 109 | ] 110 | from random import choice 111 | self.logger.log(self.logger.color('\n' + choice(phrases), self.logger.PINK)) 112 | sys.exit(0) 113 | -------------------------------------------------------------------------------- /mmlxlib/logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2012 Craig Campbell 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | class Logger(object): 19 | BLUE = 'blue' 20 | LIGHT_BLUE = 'light_blue' 21 | PINK = 'pink' 22 | YELLOW = 'yellow' 23 | WHITE = 'white' 24 | GREEN = 'green' 25 | RED = 'red' 26 | GRAY = 'gray' 27 | UNDERLINE = 'underline' 28 | ITALIC = 'italic' 29 | 30 | def __init__(self, options): 31 | self.verbose = options["verbose"] 32 | 33 | def color(self, message, color, bold=False): 34 | colors = { 35 | 'italic': '\033[3m', 36 | 'underline': '\033[4m', 37 | 'light_blue': '\033[96m', 38 | 'pink': '\033[95m', 39 | 'blue': '\033[94m', 40 | 'yellow': '\033[93m', 41 | 'green': '\033[92m', 42 | 'red': '\033[91m', 43 | 'gray': '\033[90m', 44 | 'white': '\033[0m' 45 | } 46 | 47 | end = '\033[0m' 48 | 49 | if not color in colors: 50 | return message 51 | 52 | bold_start = '\033[1m' if bold else '' 53 | 54 | return colors[color] + bold_start + message + end 55 | 56 | def log(self, message, verbose_only=False): 57 | if verbose_only and not self.verbose: 58 | return 59 | 60 | print message 61 | -------------------------------------------------------------------------------- /mmlxlib/magicmacro.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2012 Craig Campbell 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | import re 17 | import math 18 | from curve import Curve 19 | 20 | 21 | class MagicMacro(object): 22 | def __init__(self, macro): 23 | # print "creating object with macro", macro 24 | self.macro = macro 25 | self.step_size = None 26 | self.repeat_count = None 27 | self.curve_type = None 28 | 29 | def repeat(self, count): 30 | self.repeat_count = int(count) 31 | 32 | def step(self, size): 33 | self.step_size = float(size) 34 | 35 | def curve(self, type=None): 36 | if not type: 37 | type = "easeInQuad" 38 | 39 | self.curve_type = type.replace('\'', '').replace('"', '') 40 | 41 | def processRepeats(self, macro): 42 | return ((' ' + macro) * int(self.repeat_count)).replace(' ', ' ') 43 | 44 | def processSteps(self, macro): 45 | first = int(macro.split(' ')[0]) 46 | last = int(macro.split(' ').pop()) 47 | return ' '.join(self.getMagicSteps(first, last, self.step_size)) 48 | 49 | # t: current time, b: begInnIng value, c: change In value, d: duration 50 | def easeIn(self, t, b, c, d): 51 | if t == 0: 52 | return b 53 | 54 | return math.pow(2, 10 * (t / d - 1)) + b 55 | 56 | def easeInQuad(self, t, b, c, d): 57 | t = float(t) / float(d) 58 | return c * t * t + b 59 | 60 | def processCurve(self, macro): 61 | begin = float(macro.split(' ')[0]) 62 | end = float(macro.split(' ').pop()) 63 | change = float(end - begin) 64 | duration = change * (1 / self.step_size) if self.step_size is not None else change 65 | 66 | curve = Curve(begin, end, duration) 67 | if hasattr(curve, self.curve_type): 68 | return curve.render(self.curve_type) 69 | 70 | raise Exception('curve doex not exist with type: ' + self.curve_type) 71 | 72 | def getMagicSteps(self, first, last, rate): 73 | values = [] 74 | start = first 75 | if first > last: 76 | while start >= last: 77 | values.append(str(int(math.floor(start)))) 78 | start -= abs(rate) 79 | 80 | return values 81 | 82 | while start <= last: 83 | values.append(str(int(math.floor(start)))) 84 | start += rate 85 | 86 | return values 87 | 88 | def processMagicSteps(self, macro): 89 | groups = macro.split(' ') 90 | 91 | values = [] 92 | for group in groups: 93 | if not '..' in group: 94 | values.append(group) 95 | continue 96 | 97 | match = re.match(r'(\d+)(\((\+|\-)?(\.?\d+(\.\d+)?)\))?..(\d+)', group) 98 | if not match: 99 | values.append(group) 100 | continue 101 | 102 | first = int(match.group(1)) 103 | last = int(match.group(6)) 104 | rate = float(match.group(4)) if match.group(4) else 1 105 | 106 | values += self.getMagicSteps(first, last, rate) 107 | 108 | return ' '.join(values) 109 | 110 | def __str__(self): 111 | macro = self.macro 112 | 113 | if self.curve_type is not None: 114 | return self.processCurve(macro) 115 | 116 | if self.curve_type is None and self.step_size is not None: 117 | macro = self.processSteps(macro) 118 | 119 | if self.repeat_count is not None: 120 | macro = self.processRepeats(macro) 121 | 122 | macro = self.processMagicSteps(macro) 123 | return macro 124 | -------------------------------------------------------------------------------- /mmlxlib/musicbox.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2012 Craig Campbell 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | import os 17 | import subprocess 18 | import sys 19 | import shutil 20 | from warpwhistle import WarpWhistle 21 | from util import Util 22 | from instrument import Instrument 23 | from listener import Listener 24 | from logger import Logger 25 | 26 | 27 | class MusicBox(object): 28 | VERSION = '1.0.2' 29 | 30 | def processArgs(self, args, local): 31 | options = { 32 | 'verbose': False, 33 | 'open_nsf': False, 34 | 'listen': False, 35 | 'local': local, 36 | 'create_nsf': True, 37 | 'create_mml': False, 38 | 'separate_voices': False, 39 | 'start': None, 40 | 'end': None 41 | } 42 | 43 | if '--help' in args: 44 | return self.showUsage() 45 | 46 | try: 47 | for key, arg in enumerate(args): 48 | if arg == '--verbose': 49 | options['verbose'] = True 50 | elif arg == '--open-nsf': 51 | options['open_nsf'] = True 52 | elif arg == '--bob-omb': 53 | options['separate_voices'] = True 54 | elif arg == '--create-nsf': 55 | value = args[key + 1] 56 | del(args[key + 1]) 57 | options['create_nsf'] = False if value == '0' else True 58 | elif arg == '--create-mml': 59 | value = args[key + 1] 60 | del(args[key + 1]) 61 | options['create_mml'] = True if value == '1' else False 62 | elif arg == '--watch': 63 | options['listen'] = True 64 | value = args[key + 1] 65 | del(args[key + 1]) 66 | bits = value.split(':') 67 | options['start'] = bits[0] 68 | options['end'] = bits[1] if len(bits) > 1 else None 69 | else: 70 | if options['start'] is None: 71 | options['start'] = arg 72 | elif options['end'] is None: 73 | options['end'] = arg 74 | except: 75 | self.showUsage() 76 | 77 | if not options['create_nsf'] and not options['create_mml']: 78 | self.showUsage('You need to create an MML file or an NSF file') 79 | 80 | if options['start'] is None: 81 | self.showUsage('You haven\'t specified a file or directory to convert') 82 | 83 | if not Util.isFileOrDirectory(options['start']): 84 | self.showUsage(self.logger.color(options['start'], self.logger.YELLOW) + " is not a file or directory") 85 | 86 | if options['end'] is None: 87 | options['end'] = options['start'].replace('.mmlx', '.mml') 88 | 89 | options['end'] = options['end'].replace('.nsf', '.mml') 90 | 91 | return options 92 | 93 | def play(self, args, local): 94 | # create an intial logger so we can log before args are processed 95 | self.logger = Logger({"verbose": False}) 96 | self.drawLogo() 97 | 98 | options = self.processArgs(args, local) 99 | self.options = options 100 | 101 | # set up the logger with the correct options 102 | self.logger = Logger(self.options) 103 | 104 | listener = Listener(self.logger) 105 | listener.onChange(self.processFile) 106 | 107 | if os.path.isdir(options['start']) and not os.path.isdir(options['end']): 108 | os.mkdir(options['end']) 109 | 110 | if self.options['listen']: 111 | listener.watch(options['start'], options['end']) 112 | else: 113 | listener.process(options['start'], options['end']) 114 | self.logger.log(self.logger.color('Done!', self.logger.PINK)) 115 | sys.exit(0) 116 | 117 | def drawLogo(self): 118 | self.logger.log(self.logger.color('_| _| _| _| _| _| _| ', self.logger.PINK)) 119 | self.logger.log(self.logger.color('_|_| _|_| _|_| _|_| _| _| _| ', self.logger.PINK)) 120 | self.logger.log(self.logger.color('_| _| _| _| _| _| _| _| ', self.logger.PINK)) 121 | self.logger.log(self.logger.color('_| _| _| _| _| _| _| ', self.logger.PINK)) 122 | self.logger.log(self.logger.color('_| _| _| _| _|_|_|_| _| _| ', self.logger.PINK)) 123 | self.logger.log(self.logger.color(' version ' + self.logger.color(MusicBox.VERSION, self.logger.PINK, True) + '\n', self.logger.PINK)) 124 | 125 | def showUsage(self, message=None): 126 | logger = self.logger 127 | if message is not None: 128 | logger.log(logger.color('ERROR:', logger.RED)) 129 | logger.log(message + "\n") 130 | 131 | logger.log(logger.color('ARGUMENTS:', logger.WHITE, True)) 132 | logger.log(logger.color('--help', logger.WHITE) + ' shows help dialogue') 133 | logger.log(logger.color('--verbose', logger.WHITE) + ' shows verbose output') 134 | logger.log(logger.color('--open-nsf', logger.WHITE) + ' opens nsf file on save') 135 | logger.log(logger.color('--bob-omb', logger.WHITE) + ' generates a separate NSF file for each voice') 136 | logger.log(logger.color('--create-mml ' + logger.color('0', logger.YELLOW), logger.WHITE) + ' creates an MML file on save (defaults to 0)') 137 | logger.log(logger.color('--create-nsf ' + logger.color('1', logger.YELLOW), logger.WHITE) + ' creates an NSF file on save (defaults to 1)') 138 | logger.log(logger.color('--watch', logger.WHITE) + logger.color(' path/to/mmlx', logger.YELLOW) + logger.color(':', logger.GRAY) + logger.color('path/to/mml', logger.YELLOW) + ' watches for changes in first directory and compiles to second') 139 | logger.log(logger.color('\nEXAMPLES:', logger.WHITE, True)) 140 | 141 | logger.log(logger.color('watch directory for changes in .mmlx files:', logger.GRAY)) 142 | logger.log(logger.color('mmlx --watch path/to/mmlx', logger.BLUE, True)) 143 | 144 | logger.log(logger.color('\nkeep compiled files in mml directory and open .nsf file on save:', logger.GRAY)) 145 | logger.log(logger.color('mmlx --watch path/to/mmlx:path/to/mml --open-nsf', logger.BLUE, True)) 146 | 147 | logger.log(logger.color('\nwatch a single file for changes:', logger.GRAY)) 148 | logger.log(logger.color('mmlx --watch path/to/file.mmlx:path/to/otherfile.mml', logger.BLUE, True)) 149 | 150 | logger.log(logger.color('\nrun once for a single file:', logger.GRAY)) 151 | logger.log(logger.color('mmlx path/to/file.mmlx path/to/otherfile.nsf', logger.BLUE, True)) 152 | logger.log(logger.color('mmlx path/to/file.mmlx', logger.BLUE, True)) 153 | 154 | logger.log(logger.color('\nrun once for a directory:', logger.GRAY)) 155 | logger.log(logger.color('mmlx path/to/mmlx path/to/nsf', logger.BLUE, True)) 156 | logger.log('') 157 | 158 | sys.exit(1) 159 | 160 | def createNSF(self, path, open_file=False): 161 | self.logger.log('generating file: ' + self.logger.color(path.replace('.mml', '.nsf'), self.logger.YELLOW)) 162 | 163 | nes_include_path = os.path.join(os.path.dirname(__file__), 'nes_include') 164 | bin_dir = os.path.join(nes_include_path, '../../bin') 165 | 166 | # copy the nes include directory locally so we don't have to deal with permission problems 167 | if not self.options['local']: 168 | new_nes_include_path = os.path.join(os.path.dirname(path), 'nes_include') 169 | shutil.copytree(nes_include_path, new_nes_include_path) 170 | nes_include_path = new_nes_include_path 171 | 172 | os.environ['NES_INCLUDE'] = nes_include_path 173 | command = os.path.join(bin_dir, 'ppmckc') if self.options['local'] else 'ppmckc' 174 | subprocess.Popen([command, '-m1', '-i', path], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 175 | # print output.communicate() 176 | # stdout = parts[0] 177 | # stderr = parts[1] 178 | 179 | command = os.path.join(bin_dir, 'nesasm') if self.options['local'] else 'nesasm' 180 | subprocess.Popen([command, '-s', '-raw', os.path.join(nes_include_path, 'ppmck.asm')], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() 181 | # parts = output.communicate() 182 | # stdout = parts[0] 183 | # stderr = parts[1] 184 | 185 | nsf_path = os.path.join(nes_include_path, 'ppmck.nes') 186 | if not os.path.isfile(nsf_path): 187 | os.unlink('define.inc') 188 | self.logger.log('') 189 | if not self.options['local']: 190 | shutil.rmtree(nes_include_path) 191 | raise Exception('failed to create NSF file! Your MML is probably invalid.') 192 | 193 | os.rename(nsf_path, path.replace('.mml', '.nsf')) 194 | os.unlink('define.inc') 195 | os.unlink('effect.h') 196 | os.unlink(path.replace('.mml', '.h')) 197 | if not self.options['local']: 198 | shutil.rmtree(nes_include_path) 199 | 200 | if open_file and self.options['open_nsf']: 201 | subprocess.call(['open', path.replace('.mml', '.nsf')]) 202 | 203 | def processFile(self, input, output, open_file=False): 204 | Instrument.reset() 205 | 206 | self.logger.log('processing file: ' + self.logger.color(input, self.logger.YELLOW), True) 207 | content = Util.openFile(input) 208 | 209 | whistle = WarpWhistle(content, self.logger, self.options) 210 | whistle.import_directory = os.path.dirname(input) 211 | 212 | while whistle.isPlaying(): 213 | open_file = open_file and whistle.first_run 214 | 215 | song = whistle.play() 216 | 217 | new_output = output 218 | if song[1] is not None: 219 | new_output = new_output.replace('.mml', '_' + song[1] + '.mml') 220 | 221 | self.handleProcessedFile(song[0], new_output, open_file) 222 | 223 | if self.options['separate_voices']: 224 | self.logger.log("") 225 | 226 | def handleProcessedFile(self, content, output, open_file=False): 227 | if self.options['create_mml']: 228 | self.logger.log('generating file: ' + self.logger.color(output, self.logger.YELLOW)) 229 | 230 | Util.writeFile(output, content) 231 | 232 | if self.options['create_nsf']: 233 | self.createNSF(output, open_file) 234 | 235 | if not self.options['create_mml']: 236 | Util.removeFile(output) 237 | 238 | if open_file: 239 | self.logger.log("") 240 | -------------------------------------------------------------------------------- /mmlxlib/nes_include/ppmck.asm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/mmlxlib/nes_include/ppmck.asm -------------------------------------------------------------------------------- /mmlxlib/nes_include/ppmck/dpcm.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/mmlxlib/nes_include/ppmck/dpcm.h -------------------------------------------------------------------------------- /mmlxlib/nes_include/ppmck/fds.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/mmlxlib/nes_include/ppmck/fds.h -------------------------------------------------------------------------------- /mmlxlib/nes_include/ppmck/fme7.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/mmlxlib/nes_include/ppmck/fme7.h -------------------------------------------------------------------------------- /mmlxlib/nes_include/ppmck/freqdata.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/mmlxlib/nes_include/ppmck/freqdata.h -------------------------------------------------------------------------------- /mmlxlib/nes_include/ppmck/internal.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/mmlxlib/nes_include/ppmck/internal.h -------------------------------------------------------------------------------- /mmlxlib/nes_include/ppmck/mmc5.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/mmlxlib/nes_include/ppmck/mmc5.h -------------------------------------------------------------------------------- /mmlxlib/nes_include/ppmck/n106.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/mmlxlib/nes_include/ppmck/n106.h -------------------------------------------------------------------------------- /mmlxlib/nes_include/ppmck/sounddrv.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/mmlxlib/nes_include/ppmck/sounddrv.h -------------------------------------------------------------------------------- /mmlxlib/nes_include/ppmck/vrc6.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/mmlxlib/nes_include/ppmck/vrc6.h -------------------------------------------------------------------------------- /mmlxlib/nes_include/ppmck/vrc7.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccampbell/mmlx/3e2e41adabcbdfde3ef0c1424d08c0b84da6c63d/mmlxlib/nes_include/ppmck/vrc7.h -------------------------------------------------------------------------------- /mmlxlib/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2012 Craig Campbell 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | import operator 17 | import os 18 | 19 | 20 | class Util(object): 21 | 22 | @staticmethod 23 | def arrayDiff(list1, list2): 24 | if type(list1) is dict: 25 | list1 = list1.values() 26 | 27 | if type(list2) is dict: 28 | list2 = list2.values() 29 | 30 | diff = set(list1) - set(list2) 31 | 32 | return list(diff) 33 | 34 | @staticmethod 35 | def openFile(path): 36 | file = open(path, "r") 37 | content = file.read() 38 | file.close() 39 | return content 40 | 41 | @staticmethod 42 | def writeFile(path, content): 43 | file = open(path, "w") 44 | file.write(content) 45 | file.close() 46 | 47 | @staticmethod 48 | def removeFile(path): 49 | os.unlink(path) 50 | 51 | @staticmethod 52 | def sortDictionary(dictionary): 53 | return sorted(dictionary.iteritems(), key=operator.itemgetter(1)) 54 | 55 | @staticmethod 56 | def isFileOrDirectory(path): 57 | return os.path.isfile(path) or os.path.isdir(path) 58 | -------------------------------------------------------------------------------- /mmlxlib/warpwhistle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2012 Craig Campbell 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | import re 17 | import os 18 | import math 19 | from util import Util 20 | from instrument import Instrument 21 | 22 | 23 | class WarpWhistle(object): 24 | TEMPO = 'tempo' 25 | VOLUME = 'volume' 26 | TIMBRE = 'timbre' 27 | ARPEGGIO = 'arpeggio' 28 | INSTRUMENT = 'instrument' 29 | PITCH = 'pitch' 30 | OCTAVE = 'octave' 31 | SLIDE = 'slide' 32 | Q = 'q' 33 | 34 | ABSOLUTE_NOTES = 'X-ABSOLUTE-NOTES' 35 | TRANSPOSE = 'X-TRANSPOSE' 36 | COUNTER = 'X-COUNTER' 37 | X_TEMPO = 'X-TEMPO' 38 | SMOOTH = 'X-SMOOTH' 39 | N106 = 'EX-NAMCO106' 40 | FDS = 'EX-DISKFM' 41 | VRC6 = 'EX-VRC6' 42 | PITCH_CORRECTION = 'PITCH-CORRECTION' 43 | 44 | CHIP_N106 = 'N106' 45 | CHIP_FDS = 'FDS' 46 | CHIP_VRC6 = 'VRC6' 47 | 48 | def __init__(self, content, logger, options): 49 | self.first_run = True 50 | 51 | # current voice we are processing if we are processing voices separately 52 | self.process_voice = None 53 | 54 | # list of voices to process 55 | self.voices_to_process = None 56 | 57 | # list of voices 58 | self.voices = None 59 | 60 | self.content = content 61 | self.logger = logger 62 | self.options = options 63 | self.reset() 64 | 65 | def reset(self): 66 | self.current_voices = [] 67 | self.global_vars = {} 68 | self.vars = {} 69 | self.instruments = {} 70 | self.data = {} 71 | self.global_lines = [] 72 | 73 | def getDataForVoice(self, voice, key): 74 | if not voice in self.data: 75 | return None 76 | 77 | if not key in self.data[voice]: 78 | return None 79 | 80 | return self.data[voice][key] 81 | 82 | def setDataForVoice(self, voice, key, value): 83 | if not voice in self.data: 84 | self.data[voice] = {} 85 | 86 | self.data[voice][key] = value 87 | 88 | def setDataForVoices(self, voices, key, value): 89 | for voice in voices: 90 | self.setDataForVoice(voice, key, value) 91 | 92 | def getVar(self, key): 93 | if key in self.vars: 94 | return self.vars[key] 95 | 96 | return None 97 | 98 | def getGlobalVar(self, key): 99 | if key in self.global_vars: 100 | return self.global_vars[key] 101 | 102 | if key == WarpWhistle.TRANSPOSE: 103 | return 0 104 | 105 | return None 106 | 107 | def processImports(self, content): 108 | matches = re.findall(r'(@import\s{1,}(\'|\")(.*)(\2))$', content, re.MULTILINE) 109 | 110 | for match in matches: 111 | filename = match[2] if match[2].endswith('.mmlx') else match[2] + '.mmlx' 112 | disk_path = os.path.join(self.import_directory, filename) 113 | file_content = Util.openFile(disk_path) 114 | content = content.replace(match[0], file_content) 115 | 116 | self.logger.log('- stripping comments again', True) 117 | content = self.stripComments(content) 118 | 119 | if content.find('@import') > 0: 120 | content = self.processImports(content) 121 | 122 | return content 123 | 124 | def stripComments(self, content): 125 | # replace all /* comments */ 126 | content = re.sub(re.compile(r'(/\*(.*?)\*/)', re.MULTILINE | re.DOTALL), '', content) 127 | 128 | # replace all ; comments 129 | content = re.sub(re.compile(r' {0,}(;.*)$', re.MULTILINE), '', content) 130 | 131 | # replace all // comments 132 | content = re.sub(re.compile(r' {0,}(//.*)$', re.MULTILINE), '', content) 133 | 134 | # replace empty lines 135 | content = re.sub(re.compile(r'\n{2,}', re.MULTILINE), '\n', content) 136 | 137 | return content 138 | 139 | def collapseSpaces(self, content): 140 | # collapse multiple spaces into a single space 141 | return re.sub(re.compile(' {2,}', re.MULTILINE), ' ', content) 142 | 143 | def removeBlankLines(self, content): 144 | return re.sub(r'\n{2,}', '\n', content) 145 | 146 | def processGlobalVariables(self, content): 147 | matches = re.findall(r'(^#([-A-Z0-9]+)( {1,}(.*))?\n)', content, re.MULTILINE) 148 | for match in matches: 149 | if match[1] == WarpWhistle.TRANSPOSE: 150 | self.global_vars[match[1]] = int(match[3]) if match[3] else 0 151 | else: 152 | self.global_vars[match[1]] = match[3] or True 153 | 154 | if match[1].startswith('X-'): 155 | content = content.replace(match[0], '') 156 | else: 157 | self.global_lines.append(match[0]) 158 | 159 | if match[1] == WarpWhistle.COUNTER: 160 | Instrument.reset(match[3]) 161 | 162 | return content 163 | 164 | def isReserved(self, var): 165 | # single letter match 166 | if re.match(r'^[A-Z]{1,2}$', var): 167 | return True 168 | 169 | # volume macro 170 | if re.match(r'^v\d+$', var): 171 | return True 172 | 173 | # rest 174 | if re.match(r'^r\d+$', var): 175 | return True 176 | 177 | # wait 178 | if re.match(r'^w\d+$', var): 179 | return True 180 | 181 | # pitch macro 182 | if re.match(r'^EP\d+$', var): 183 | return True 184 | 185 | # arpeggio macro 186 | if re.match(r'^EN\d+$', var): 187 | return True 188 | 189 | # self delay macro 190 | if re.match(r'^SD\d+$', var): 191 | return True 192 | 193 | reserved = ['EPOF', 'ENOF', 'SDOF', 'w', 'r'] 194 | 195 | return var in reserved 196 | 197 | def processLocalVariables(self, content): 198 | matches = re.findall(r'(^([a-zA-Z]{1}([a-zA-Z0-9_]+)?)\s{0,}=\s{0,}(.*)\n)', content, re.MULTILINE) 199 | for match in matches: 200 | 201 | if self.isReserved(match[1]): 202 | raise Exception('variable ' + match[1] + ' is reserved') 203 | 204 | self.vars[match[1]] = match[3] 205 | 206 | content = content.replace(match[0], '') 207 | 208 | return content 209 | 210 | def processVariables(self, content): 211 | content = self.processGlobalVariables(content) 212 | content = self.processLocalVariables(content) 213 | return content 214 | 215 | def addInstrument(self, name, content): 216 | if name == 'end': 217 | raise Exception('end is a reserved word and connt be used for an instrument') 218 | 219 | lines = content.strip().split('\n') 220 | data = {} 221 | 222 | for line in lines: 223 | line = line.strip() 224 | match = re.match(r'^@extends {1,}(\'|\")(.*)(\1)$', line) 225 | if match: 226 | data["extends"] = match.group(2) 227 | continue 228 | 229 | data[line.split(':', 1)[0].strip()] = line.split(':', 1)[1].strip() 230 | 231 | self.instruments[name] = Instrument(data) 232 | 233 | def updateInstruments(self): 234 | for name in self.instruments: 235 | instrument = self.instruments[name] 236 | while instrument.hasParent(): 237 | instrument.inherit(self.instruments[instrument.getParent()]) 238 | 239 | def processInstruments(self, content): 240 | matches = re.findall(r'(^([a-zA-Z0-9-_]+):( {0,}(\n( {4}|\t)(.*))+)\n)', content, re.MULTILINE) 241 | for match in matches: 242 | self.addInstrument(match[1].lower(), match[2]) 243 | content = content.replace(match[0], '') 244 | 245 | self.updateInstruments() 246 | return content 247 | 248 | def getVoicesForChip(self, chip): 249 | if chip == WarpWhistle.CHIP_N106: 250 | return { 251 | 'A': 'P', 252 | 'B': 'Q', 253 | 'C': 'R', 254 | 'D': 'S', 255 | 'E': 'T', 256 | 'F': 'U', 257 | 'G': 'V', 258 | 'H': 'W' 259 | } 260 | 261 | if chip == WarpWhistle.CHIP_FDS: 262 | return { 263 | 'A': 'F' 264 | } 265 | 266 | if chip == WarpWhistle.CHIP_VRC6: 267 | return { 268 | 'A': 'M', 269 | 'B': 'N', 270 | 'C': 'O' 271 | } 272 | 273 | def getVoiceTranslation(self, chip, voice): 274 | voices = self.getVoicesForChip(chip) 275 | 276 | if not voice in voices: 277 | return None 278 | 279 | return voices[voice] 280 | 281 | def getVoiceFor(self, chip, voices): 282 | final_voices = [] 283 | voices = list(voices) 284 | for voice in voices: 285 | new_voice = self.getVoiceTranslation(chip, voice) 286 | if new_voice is None: 287 | raise Exception('voice: ' + voice + ' is outside of the voice range for chip: ' + chip) 288 | 289 | final_voices.append(new_voice) 290 | 291 | return ''.join(final_voices) 292 | 293 | def processExpansionVoices(self, content): 294 | """finds any special voices (such as N106-AB) and converts them to the proper voice names""" 295 | matches = re.findall(r'((N106|FDS|VRC6)-([A-Z]+) )', content) 296 | for match in matches: 297 | content = content.replace(match[0], self.getVoiceFor(match[1], match[2]) + ' ') 298 | 299 | return content 300 | 301 | def renderTempo(self, content): 302 | tempo = self.getGlobalVar(WarpWhistle.X_TEMPO) 303 | if tempo is None: 304 | return content 305 | 306 | return self.addToMml(content, "".join(self.voices) + " t" + str(tempo) + "\n") 307 | 308 | def renderInstruments(self, content): 309 | if not Instrument.hasBeenUsed(): 310 | return content 311 | 312 | # find the last #BLOCK on the top of the file and render the instruments below it 313 | return self.addToMml(content, Instrument.render()) 314 | 315 | def renderN106(self, content): 316 | n106_voices = self.getVoicesForChip(WarpWhistle.CHIP_N106).values() 317 | n106_voices.sort() 318 | 319 | n106_count = 0 320 | for voice in self.voices: 321 | if voice in n106_voices: 322 | index = n106_voices.index(voice) + 1 323 | n106_count = max(n106_count, index) 324 | 325 | if n106_count > 0: 326 | # in order to stay on pitch it has to be 1,2,4 or 8 327 | if n106_count == 3: 328 | n106_count = 4 329 | 330 | if n106_count in [5, 6, 7]: 331 | n106_count = 8 332 | 333 | if not WarpWhistle.N106 in self.global_vars: 334 | self.global_vars[WarpWhistle.N106] = str(n106_count) 335 | content = self.addToMml(content, '#' + WarpWhistle.N106 + ' ' + str(n106_count) + '\n', True) 336 | 337 | if not WarpWhistle.PITCH_CORRECTION in self.global_vars: 338 | self.global_vars[WarpWhistle.PITCH_CORRECTION] = True 339 | content = self.addToMml(content, '#' + WarpWhistle.PITCH_CORRECTION + '\n', True) 340 | 341 | return content 342 | 343 | def getExpForChip(self, chip): 344 | if chip == WarpWhistle.CHIP_N106: 345 | return WarpWhistle.N106 346 | elif chip == WarpWhistle.CHIP_FDS: 347 | return WarpWhistle.FDS 348 | elif chip == WarpWhistle.CHIP_VRC6: 349 | return WarpWhistle.VRC6 350 | 351 | def renderForChip(self, chip, content): 352 | chip_voices = self.getVoicesForChip(chip).values() 353 | chip_voices.sort() 354 | 355 | used = False 356 | for voice in self.voices: 357 | if voice in chip_voices: 358 | used = True 359 | break 360 | 361 | exp = self.getExpForChip(chip) 362 | if used and not exp in self.global_vars: 363 | self.global_vars[exp] = True 364 | content = self.addToMml(content, '#' + exp + '\n', True) 365 | 366 | return content 367 | 368 | def renderExpansionChips(self, content): 369 | content = self.renderN106(content) 370 | content = self.renderForChip(WarpWhistle.CHIP_FDS, content) 371 | content = self.renderForChip(WarpWhistle.CHIP_VRC6, content) 372 | 373 | return content 374 | 375 | def addToMml(self, content, string, is_global_line=False): 376 | try: 377 | last_global_declaration = self.global_lines[-1] 378 | except: 379 | # if there are no global lines then prepend 380 | return string + content 381 | 382 | if is_global_line: 383 | self.global_lines.append(string) 384 | 385 | return content.replace(last_global_declaration, last_global_declaration + string) 386 | 387 | def replaceVariables(self, content): 388 | for key in self.vars: 389 | pattern = '((?<=\s)|(?<=\[))' + key + '(?=\s|\Z|\])' 390 | content = re.sub(re.compile(pattern, re.MULTILINE), self.vars[key], content) 391 | 392 | return content 393 | 394 | def isUndefinedVariable(self, var): 395 | return False 396 | 397 | # def processVoice(self, voice, content): 398 | # regex = '(' + voice + '\s{0,}(.*?)(?=(\n[A-Z^' + voice + ']{1,2}\s|\n?\Z)))' 399 | # matches = re.findall(regex, content, re.MULTILINE | re.DOTALL) 400 | # print matches 401 | # return content 402 | 403 | def moveToOctave(self, new_octave, last_octave): 404 | diff = new_octave - last_octave 405 | ticks = abs(diff) 406 | symbol = '>' if diff > 0 else '<' 407 | return symbol * ticks 408 | 409 | def getNumberForNote(self): 410 | return { 411 | 'c': 0, 412 | 'c+': 1, 413 | 'd-': 1, 414 | 'd': 2, 415 | 'd+': 3, 416 | 'e-': 3, 417 | 'e': 4, 418 | 'f-': 4, 419 | 'e+': 5, 420 | 'f': 5, 421 | 'f+': 6, 422 | 'g-': 6, 423 | 'g': 7, 424 | 'g+': 8, 425 | 'a-': 8, 426 | 'a': 9, 427 | 'a+': 10, 428 | 'b-': 10, 429 | 'b': 11 430 | } 431 | 432 | def getNoteForNumber(self): 433 | return { 434 | 0: 'c', 435 | 1: 'c+', 436 | 2: 'd', 437 | 3: 'd+', 438 | 4: 'e', 439 | 5: 'f', 440 | 6: 'f+', 441 | 7: 'g', 442 | 8: 'g+', 443 | 9: 'a', 444 | 10: 'a+', 445 | 11: 'b' 446 | } 447 | 448 | def isNoiseChannel(self): 449 | return self.current_voices[0] == 'D' 450 | 451 | def getFrequency(self, note, octave): 452 | frequencies = [ 453 | 0x06AE, 454 | 0x064E, 455 | 0x05F4, 456 | 0x059E, 457 | 0x054E, 458 | 0x0501, 459 | 0x04B9, 460 | 0x0476, 461 | 0x0436, 462 | 0x03F9, 463 | 0x03C0, 464 | 0x038A 465 | ] 466 | 467 | index = self.getNumberForNote()[note] 468 | frequency = frequencies[index] >> (octave - 2) 469 | return frequency 470 | 471 | def calculateN106OctaveShift(self, channel_count, waveform): 472 | if waveform is None: 473 | return 0 474 | 475 | # my math skills are so lacking these days this is the number of octaves to shift 476 | # based on the waveform sample count and the channel count 477 | shift = { 478 | 4: 4, 479 | 8: 3, 480 | 16: 2, 481 | 32: 1, 482 | 64: 0, 483 | 128: -1, 484 | 256: -2 485 | } 486 | 487 | number = channel_count * len(waveform.strip().split(' ')) 488 | return shift[number] 489 | 490 | def slide(self, start_data, end_data): 491 | N106_channels = self.getGlobalVar(WarpWhistle.N106) 492 | shift = 0 493 | if N106_channels is not None and len(N106_channels): 494 | active_instruments = self.getDataForVoice(self.current_voices[0], WarpWhistle.INSTRUMENT) 495 | waveform = None 496 | for instrument in active_instruments: 497 | if hasattr(instrument, 'waveform'): 498 | waveform = instrument.waveform 499 | break 500 | 501 | shift = self.calculateN106OctaveShift(int(N106_channels), waveform) 502 | 503 | match = re.match(r'^(\[+)?([a-g](\+|\-)?)(.*)$', start_data['note']) 504 | start_data['note'] = match.group(2) 505 | start_data['append'] = match.group(4) 506 | 507 | # total amount we need to move 508 | slide_amount = self.getFrequency(start_data['note'], start_data['octave'] + shift) - self.getFrequency(end_data['note'], end_data['octave'] + shift) 509 | 510 | # song tempo 511 | tempo = self.getDataForVoice(self.current_voices[0], WarpWhistle.TEMPO) 512 | 513 | # make sure if tempo is none we default to 120 514 | if tempo is None: 515 | tempo = 120 516 | 517 | # @todo this is NTSC, need to add support for PAL - 50 FPS 518 | frames_per_second = 60 519 | 520 | # default to 16th note unless speed is specified 521 | slide_note_duration = 16 if start_data['speed'] is None else start_data['speed'] 522 | 523 | # figure out the slide duration in seconds 524 | beats_per_second = float(tempo) / float(60) 525 | slides_per_second = (float(slide_note_duration) / float(4)) * beats_per_second 526 | slide_duration = float(1) / float(slides_per_second) 527 | 528 | frames_for_slide = int(math.floor(frames_per_second * slide_duration)) 529 | 530 | steps = frames_for_slide 531 | distance_per_step = int(math.floor(slide_amount / frames_for_slide)) 532 | remainder = slide_amount - steps * distance_per_step 533 | 534 | # print 'STEPS',steps 535 | # print 'DISTANCE_PER_STEP',distance_per_step 536 | # print 'REMAINDER',remainder 537 | 538 | increase_at = steps - remainder 539 | pitch_macro = [] 540 | for x in range(0, steps): 541 | if x < increase_at: 542 | pitch_macro.append(str(distance_per_step)) 543 | continue 544 | 545 | pitch_macro.append(str(distance_per_step + 1)) 546 | 547 | pitch_macro = ' '.join(pitch_macro) + ' 0' 548 | instrument = Instrument({ 549 | 'pitch': pitch_macro 550 | }) 551 | 552 | macro = instrument.getPitchMacro() 553 | 554 | # no longer need to slide 555 | self.setDataForVoices(self.current_voices, WarpWhistle.SLIDE, None) 556 | 557 | append_before = end_data['append'] 558 | append_after = '' 559 | match = re.match(r'(.*)(\](.*))', end_data['append']) 560 | if match: 561 | 562 | if match.group(1): 563 | append_before = match.group(1) 564 | 565 | if match.group(2): 566 | append_after = match.group(2) 567 | 568 | # print 'START',start_data 569 | # print 'END',end_data 570 | # print "" 571 | 572 | octave_diff = start_data['octave'] - end_data['octave'] 573 | 574 | diff = Util.arrayDiff(self.current_voices, ['A', 'B', 'C']) 575 | 576 | smooth_start = '' 577 | smooth_end = '' 578 | if len(diff) == 0: 579 | smooth = self.getGlobalVar(WarpWhistle.SMOOTH) 580 | 581 | if smooth: 582 | smooth_start = ' SM ' 583 | smooth_end = ' SMOF' 584 | 585 | return self.getOctaveShift(octave_diff) + smooth_start + macro + ' ' + start_data['note'] + append_before + ' EPOF' + smooth_end + append_after + ' ' + self.getOctaveShift(-octave_diff) 586 | 587 | def getOctaveShift(self, ticks): 588 | char = '<' if ticks < 0 else '>' 589 | return abs(ticks) * char 590 | 591 | def transposeNote(self, note, octave, amount, append): 592 | if append is None: 593 | append = '' 594 | 595 | start_data = self.getDataForVoice(self.current_voices[0], WarpWhistle.SLIDE) 596 | 597 | new_note = '' 598 | if amount == 0 or self.isNoiseChannel(): 599 | if start_data: 600 | return self.slide(start_data, {'note': note, 'append': append, 'octave': octave}) 601 | 602 | return note + append 603 | 604 | new_note_number = self.getNumberForNote()[note] + amount 605 | 606 | ticks = 0 607 | while new_note_number < 0: 608 | new_note += '< ' 609 | ticks += 1 610 | new_note_number = new_note_number + 12 611 | 612 | while new_note_number > 11: 613 | ticks -= 1 614 | new_note += '> ' 615 | new_note_number = new_note_number - 12 616 | 617 | note_name = self.getNoteForNumber()[new_note_number] 618 | new_note += note_name 619 | new_note += append + ' ' + self.getOctaveShift(ticks) 620 | 621 | return new_note 622 | 623 | def processWord(self, word, next_word, prev_word): 624 | if not word: 625 | return word 626 | 627 | valid_commands = ['EPOF', 'ENOF', 'MPOF', 'PS', 'SDQR', 'SDOF', 'MHOF', 'SM', 'SMOF', 'EHOF'] 628 | if word in valid_commands: 629 | if self.ignore: 630 | return "" 631 | 632 | return word 633 | 634 | # matches a voice declaration 635 | if re.match(r'[A-Z]{1,}$', word): 636 | 637 | self.current_voices = list(word) 638 | 639 | # processing everything, keep going 640 | if self.process_voice is None: 641 | return word 642 | 643 | # if we are processing a specific voice 644 | # and we are on that voice 645 | if self.process_voice in self.current_voices: 646 | self.ignore = False 647 | return self.process_voice 648 | 649 | # if we are processing a specific voice and we are not on that voice 650 | self.ignore = True 651 | return "" 652 | 653 | if self.ignore: 654 | return "" 655 | 656 | # slides for portamento 657 | match = re.match(r'^\/([0-9]+)?$', word) 658 | if match: 659 | 660 | # calculate the previous note 661 | prev_note = self.processWord(prev_word, None, None) 662 | 663 | # figure out what octave we are at now 664 | start_octave = self.getDataForVoice(self.current_voices[0], WarpWhistle.OCTAVE) 665 | 666 | self.setDataForVoices(self.current_voices, WarpWhistle.SLIDE, {'note': prev_note, 'octave': start_octave, 'speed': match.group(1)}) 667 | 668 | return '' 669 | 670 | # matches a tempo declaration 671 | if re.match(r't\d+$', word): 672 | self.setDataForVoices(self.current_voices, WarpWhistle.TEMPO, int(word[1:])) 673 | return word 674 | 675 | # volume change 676 | if re.match(r'@v\d+$', word) and next != '=': 677 | self.setDataForVoices(self.current_voices, WarpWhistle.VOLUME, int(word[2:])) 678 | return word 679 | 680 | # timbre change 681 | if re.match(r'@@\d+$', word): 682 | self.setDataForVoices(self.current_voices, WarpWhistle.TIMBRE, int(word[2:])) 683 | return word 684 | 685 | # arpeggio change 686 | if re.match(r'EN\d+$', word): 687 | self.setDataForVoices(self.current_voices, WarpWhistle.ARPEGGIO, int(word[2:])) 688 | return word 689 | 690 | # pitch change 691 | if re.match(r'EP\d+$', word): 692 | self.setDataForVoices(self.current_voices, WarpWhistle.PITCH, int(word[2:])) 693 | return word 694 | 695 | # q change 696 | if re.match(r'q[0-8]$', word): 697 | self.setDataForVoices(self.current_voices, WarpWhistle.Q, int(word[1:])) 698 | return word 699 | 700 | # direct timbre 701 | if re.match(r'@\d+$', word): 702 | self.setDataForVoices(self.current_voices, WarpWhistle.TIMBRE, int(word[1:])) 703 | return word 704 | 705 | # explicit octave change (with o4 o3 etc) 706 | if re.match(r'o\d+$', word): 707 | self.setDataForVoices(self.current_voices, WarpWhistle.OCTAVE, int(word[1:])) 708 | return word 709 | 710 | # octave change with > or < or >>> 711 | if re.match(r'\>+|\<+$', word): 712 | direction = word[0] 713 | count = len(word) 714 | current_octave = self.getDataForVoice(self.current_voices[0], WarpWhistle.OCTAVE) 715 | 716 | if current_octave is None: 717 | current_octave = 0 718 | 719 | self.setDataForVoices(self.current_voices, WarpWhistle.OCTAVE, current_octave + (count if direction == '>' else -count)) 720 | 721 | return word 722 | 723 | # dmc declaration 724 | match = re.match(r'(\{\s{0,})?(\'|\")(.*\.dmc)(\2)\s{0,},', word) 725 | if match: 726 | mmlx_dir = self.options['start'] if os.path.isdir(self.options['start']) else os.path.dirname(self.options['start']) 727 | new_path = os.path.join(mmlx_dir, match.group(3)) 728 | new_word = '' 729 | 730 | if match.group(1): 731 | new_word += match.group(1) 732 | 733 | new_word += match.group(2) + new_path + match.group(2) + ',' 734 | 735 | return new_word 736 | 737 | # rewrite special voices for mmlx such as c4 or G+,4^8 738 | # to use this put the line X-ABSOLUTE-NOTES at the top of your mmlx file 739 | match = re.match(r'(\[+)?([A-Ga-g]{1})(\+|\-)?(\d{1,2})?(,(\d+\.?)(\^[0-9\^]+)?)?([\]\d]+)?$', word) 740 | if match and self.getGlobalVar(WarpWhistle.ABSOLUTE_NOTES): 741 | is_noise_channel = self.current_voices[0] == 'D' 742 | 743 | if is_noise_channel and not "," in word: 744 | return word 745 | 746 | new_word = "" 747 | 748 | octave = match.group(4) if not is_noise_channel else 0 749 | 750 | current_octave = self.getDataForVoice(self.current_voices[0], WarpWhistle.OCTAVE) 751 | 752 | if current_octave is None and not is_noise_channel: 753 | new_word += 'o' + octave + ' ' 754 | elif not is_noise_channel and octave and int(octave) != current_octave: 755 | new_word += self.moveToOctave(int(octave), current_octave) + ' ' 756 | 757 | if octave: 758 | self.setDataForVoices(self.current_voices, WarpWhistle.OCTAVE, int(octave)) 759 | current_octave = int(octave) 760 | 761 | # [[[ 762 | if match.group(1): 763 | new_word += match.group(1) 764 | 765 | note = "" 766 | 767 | # note 768 | note += match.group(2).lower() 769 | 770 | # accidental 771 | if match.group(3): 772 | note += match.group(3) 773 | 774 | append = "" 775 | 776 | # tack on the note length 777 | if match.group(6): 778 | append += match.group(6) 779 | 780 | # tack on any ties (such as ^8^16) 781 | if match.group(7): 782 | append += match.group(7) 783 | 784 | # tack on the final repeat value if it is present (]4) 785 | if match.group(8): 786 | append += match.group(8) 787 | 788 | new_word += self.transposeNote(note, current_octave, self.getGlobalVar(WarpWhistle.TRANSPOSE), append) 789 | 790 | return new_word 791 | 792 | # regular note 793 | match = re.match(r'(\[+)?([a-g]{1}(\+|\-)?)([\.0-9\^]+)?([\]\d+]+)?$', word) 794 | if match: 795 | if "," in word and not self.getGlobalVar(WarpWhistle.ABSOLUTE_NOTES): 796 | raise Exception('In order to use absolute notes you have to specify X-ABSOLUTE-NOTES') 797 | 798 | current_octave = self.getDataForVoice(self.current_voices[0], WarpWhistle.OCTAVE) 799 | 800 | new_note = "" 801 | if match.group(1): 802 | new_note += match.group(1) 803 | 804 | append = '' 805 | if match.group(4): 806 | append = match.group(4) 807 | 808 | if match.group(5): 809 | append += match.group(5) 810 | 811 | new_note += self.transposeNote(match.group(2), current_octave, self.getGlobalVar(WarpWhistle.TRANSPOSE), append) 812 | 813 | return new_note 814 | 815 | # instrument 816 | match = re.match(r'^(\[+)?(\+)?@([a-zA-Z0-9-_]+)([\]\d+]+)?$', word) 817 | if match: 818 | 819 | new_word = '' 820 | 821 | # special case if you do @end you can end the currently active instruments 822 | if match.group(3) == 'end': 823 | active_instruments = self.getDataForVoice(self.current_voices[0], WarpWhistle.INSTRUMENT) 824 | 825 | for active_instrument in active_instruments: 826 | new_word += active_instrument.end(self) 827 | 828 | self.setDataForVoices(self.current_voices, WarpWhistle.INSTRUMENT, []) 829 | return new_word 830 | 831 | # not a valid instrument 832 | if not match.group(3).lower() in self.instruments: 833 | return word 834 | 835 | new_instrument = self.instruments[match.group(3).lower()] 836 | 837 | if 'O' in self.current_voices and hasattr(new_instrument, 'timbre'): 838 | raise Exception('VRC6 sawtooth (voice O) does not support timbre attribute') 839 | 840 | chip = new_instrument.getChip() 841 | diff = [] 842 | if chip is not None: 843 | diff = Util.arrayDiff(self.current_voices, self.getVoicesForChip(chip).values()) 844 | 845 | if len(diff): 846 | diff.sort() 847 | words = ('voice', 'does') if len(diff) == 1 else ('voices', 'do') 848 | raise Exception(words[0] + ' ' + ', '.join(diff) + ' ' + words[1] + ' not support instruments using chip: ' + chip) 849 | 850 | active_instruments = self.getDataForVoice(self.current_voices[0], WarpWhistle.INSTRUMENT) 851 | 852 | if active_instruments is None: 853 | active_instruments = [] 854 | 855 | if match.group(1): 856 | new_word += match.group(1) 857 | 858 | if len(active_instruments) and not match.group(2): 859 | for active_instrument in active_instruments: 860 | new_word += active_instrument.end(self) 861 | 862 | active_instruments = [] 863 | 864 | active_instruments.append(new_instrument) 865 | self.setDataForVoices(self.current_voices, WarpWhistle.INSTRUMENT, active_instruments) 866 | 867 | new_word += new_instrument.start(self) 868 | 869 | if match.group(4): 870 | new_word += match.group(4) 871 | 872 | return new_word 873 | 874 | if self.isUndefinedVariable(word): 875 | raise Exception('variable ' + word + ' is undefined') 876 | 877 | # print "PROCESS:",word 878 | # print "PREV:",prev_word 879 | # print "NEXT:",next_word 880 | # print "" 881 | return word 882 | 883 | def processLine(self, line): 884 | self.ignore = False 885 | 886 | words = line.split(' ') 887 | new_words = [] 888 | 889 | for key, word in enumerate(words): 890 | next_word = None 891 | prev_word = None 892 | 893 | if len(words) > key + 1: 894 | next_word = words[key + 1] 895 | 896 | if len(words) > key - 1: 897 | prev_word = words[key - 1] 898 | 899 | new_words.append(self.processWord(word, next_word, prev_word)) 900 | 901 | return ' '.join(new_words) 902 | 903 | def findVoices(self, content): 904 | matches = re.findall(r'^([A-Z]{1,}) ', content, re.MULTILINE) 905 | 906 | voices = [] 907 | for match in matches: 908 | new_voices = list(match) 909 | for voice in new_voices: 910 | if not voice in voices: 911 | voices.append(voice) 912 | 913 | return voices 914 | 915 | def process(self, content): 916 | self.logger.log('- stripping comments', True) 917 | content = self.stripComments(content) 918 | 919 | self.logger.log('- proccessing imports', True) 920 | content = self.processImports(content) 921 | 922 | self.logger.log('- parsing variables', True) 923 | content = self.processVariables(content) 924 | 925 | self.logger.log('- applying variables', True) 926 | content = self.replaceVariables(content) 927 | 928 | self.logger.log('- parsing instruments', True) 929 | content = self.processInstruments(content) 930 | 931 | self.logger.log('- collapsing spaces', True) 932 | content = self.collapseSpaces(content) 933 | 934 | self.logger.log('- processing expansion voices', True) 935 | content = self.processExpansionVoices(content) 936 | 937 | self.voices = self.findVoices(content) 938 | content = self.renderTempo(content) 939 | 940 | content = self.renderExpansionChips(content) 941 | 942 | if not self.first_run: 943 | if self.voices_to_process is None: 944 | self.voices_to_process = self.voices 945 | 946 | if len(self.voices_to_process): 947 | self.process_voice = self.voices_to_process.pop(0) 948 | 949 | if self.process_voice: 950 | self.logger.log('processing voice: ' + self.process_voice, True) 951 | 952 | lines = content.split('\n') 953 | new_lines = [] 954 | for line in lines: 955 | new_lines.append(self.processLine(line)) 956 | 957 | content = '\n'.join(new_lines) 958 | 959 | content = self.renderInstruments(content) 960 | 961 | self.logger.log('- replacing unneccessary octave shifts', True) 962 | patterns = ['><', '> <', '<>', '< >'] 963 | for pattern in patterns: 964 | while content.find(pattern) > 0: 965 | content = content.replace(pattern, '') 966 | 967 | self.logger.log('- replacing extra spaces', True) 968 | content = self.collapseSpaces(content) 969 | 970 | self.logger.log('- removing blank lines', True) 971 | content = self.removeBlankLines(content) 972 | 973 | self.first_run = False 974 | 975 | return content 976 | 977 | def isPlaying(self): 978 | if self.first_run: 979 | return True 980 | 981 | if not self.options['separate_voices']: 982 | return False 983 | 984 | return self.voices_to_process is None or len(self.voices_to_process) != 0 985 | 986 | def play(self): 987 | counter = self.getGlobalVar(WarpWhistle.COUNTER) or 0 988 | Instrument.reset(counter) 989 | self.reset() 990 | return (self.process(self.content), self.process_voice) 991 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup( 6 | name = 'mmlx', 7 | version = '1.0.2', 8 | description = 'Nintendo NES chiptune programming language', 9 | long_description = 'MMLX stands for MML eXtended. It is an extended version of Music Macro Language that generates MML and NSF files for Nintendo NES music composing', 10 | author = 'Craig Campbell', 11 | author_email = 'iamcraigcampbell@gmail.com', 12 | url = 'https://github.com/ccampbell/mmlx', 13 | download_url = 'https://github.com/ccampbell/mmlx/zipball/1.0.2', 14 | license = 'Apache Software License', 15 | packages = ['mmlxlib'], 16 | package_data = {'mmlxlib': ['nes_include/ppmck.asm', 'nes_include/ppmck/*']}, 17 | scripts = ['bin/mmlx', 'bin/ppmckc', 'bin/nesasm'] 18 | ) 19 | -------------------------------------------------------------------------------- /tests/chips/n106.mml: -------------------------------------------------------------------------------- 1 | #TITLE N106 Demo 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | #EX-NAMCO106 4 5 | #PITCH-CORRECTION 6 | @v0 = { 10 } 7 | @v1 = { 12 } 8 | @v2 = { 6 } 9 | @N0 = { 00, 15 15 15 15 0 0 0 0 } 10 | @N1 = { 01, 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 } 11 | PQR t130 12 | P o2 @v0 @@0 a > c e a e c e g < a > c e a g d < b g f a > c f e < b g+ e a1 13 | Q o1 l8 @v1 @@1 [a > a]4 [c > c]4 << [a > a]4 < [g > g]4 < [f > f]4 < [e > e]4 < a1 14 | R o4 @v0 @@0 @v2 r1 c c c e r1 < b g g b r1^1 c1 15 | -------------------------------------------------------------------------------- /tests/chips/n106.mmlx: -------------------------------------------------------------------------------- 1 | #TITLE N106 Demo 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | 5 | /** 6 | * You can use the X-TEMPO declaration if you don't know all the voice names 7 | * that will end up in your final document (for example if using expansion chips) 8 | */ 9 | #X-TEMPO 130 10 | 11 | /** 12 | * instruments 13 | */ 14 | square: 15 | volume: 10 16 | chip: N106 17 | waveform: 15 15 15 15 0 0 0 0 18 | 19 | sawtooth: 20 | volume: 12 21 | chip: N106 22 | waveform: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 23 | buffer: 1 24 | 25 | soft: 26 | volume: 6 27 | 28 | N106-A o2 @square a > c e a e c e g < a > c e a g d < b g f a > c f e < b g+ e a1 29 | N106-B o1 l8 @sawtooth [a > a]4 [c > c]4 << [a > a]4 < [g > g]4 < [f > f]4 < [e > e]4 < a1 30 | N106-C o4 @square +@soft r1 c c c e r1 < b g g b r1^1 c1 31 | -------------------------------------------------------------------------------- /tests/chips/vrc6.mml: -------------------------------------------------------------------------------- 1 | #TITLE VRC6 Demo 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | #EX-VRC6 5 | @0 = { 4 } 6 | @v0 = { 10 8 5 3 0 } 7 | @v1 = { 10 9 8 7 6 5 4 3 2 0 } 8 | @v2 = { 45 } 9 | @v3 = { 40 } 10 | @v4 = { 50 } 11 | @v5 = { 63 } 12 | @EP0 = { -15 -15 | 0 1 0 } 13 | @EP1 = { 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 0 } 14 | @EP2 = { 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 0 } 15 | @EP3 = { -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -3 -3 -3 -3 -3 -3 -3 -3 0 } 16 | @EP4 = { 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 0 } 17 | @EP5 = { 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 0 } 18 | @MP0 = { 2 4 8 } 19 | CDMNO t75 20 | C o4 |: c2. c2. a2. a2. f2. f2. g2. g2. :|2 c2. 21 | D l16 [@v0 q1 d d d @v1 EP0 q8 g EPOF @v0 q1 d d]32 22 | M |: o3 @@0 @v2 c EP1 c2 EPOF g > MP0 MPOF < a. > c. e MP0 EP3 e2 EPOF 23 | M MPOF f EP4 f2 EPOF > f. < MP0 a. < MPOF g. g. b > MP0 :|2 24 | M MPOF c2. 25 | N |: @@0 @v2 @v3 o4 e. c. e. e. c. e. c. a. 26 | N a. a. a. f. b. b. d. b. :|2 27 | N e2. 28 | O |: o2 l16 @v4 q2 [c c c @v5 q4 c @v4 q2 c c]4 < [a a a @v5 q4 a @v4 q2 a a]4 29 | O [f f f @v5 q4 f @v4 q2 f f]4 [g g g @v5 q4 g @v4 q2 g g]4 :|2 30 | O q8 > c2. 31 | -------------------------------------------------------------------------------- /tests/chips/vrc6.mmlx: -------------------------------------------------------------------------------- 1 | #TITLE VRC6 Demo 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | #X-TEMPO 75 5 | 6 | square: 7 | volume: 45 8 | chip: VRC6 9 | timbre: 4 10 | 11 | soft: 12 | volume: 40 13 | 14 | sawtooth: 15 | volume: 50 16 | chip: VRC6 17 | q: 2 18 | 19 | long: 20 | volume: 63 21 | q: 4 22 | 23 | vibrato: 24 | vibrato: 2 4 8 25 | 26 | hi-hat: 27 | adsr: 0 0 10 5 28 | q: 1 29 | 30 | snare: 31 | adsr: 0 0 10 10 32 | pitch: -15 -15 | 0 1 0 33 | q: 8 34 | 35 | C o4 |: c2. c2. a2. a2. f2. f2. g2. g2. :|2 c2. 36 | D l16 [@hi-hat d d d @snare g @hi-hat d d]32 37 | 38 | VRC6-A |: o3 @square c /8 e2 g /8 > +@vibrato c2 @square < a. > c. e /8 +@vibrato c2 39 | VRC6-A @square f /8 > c2 f. < +@vibrato a. < @square g. g. b /8 > +@vibrato d2 :|2 40 | VRC6-A @square c2. 41 | 42 | VRC6-B |: @square +@soft o4 e. c. e. e. c. e. c. a. 43 | VRC6-B a. a. a. f. b. b. d. b. :|2 44 | VRC6-B e2. 45 | 46 | VRC6-C |: o2 l16 @sawtooth [c c c +@long c @sawtooth c c]4 < [a a a +@long a @sawtooth a a]4 47 | VRC6-C [f f f +@long f @sawtooth f f]4 [g g g +@long g @sawtooth g g]4 :|2 48 | VRC6-C q8 > c2. 49 | -------------------------------------------------------------------------------- /tests/features/_import1.mmlx: -------------------------------------------------------------------------------- 1 | A c1 2 | -------------------------------------------------------------------------------- /tests/features/_import2.mmlx: -------------------------------------------------------------------------------- 1 | B e1 2 | @import "_import3.mmlx" 3 | -------------------------------------------------------------------------------- /tests/features/_import3.mmlx: -------------------------------------------------------------------------------- 1 | C a1 2 | 3 | // @import that should be ignored 4 | -------------------------------------------------------------------------------- /tests/features/adsr.mml: -------------------------------------------------------------------------------- 1 | @v0 = { 15 14 12 10 9 7 5 4 2 0 } 2 | @v1 = { 0 2 4 5 7 9 10 12 14 15 15 15 14 14 13 13 12 12 11 10 10 10 9 9 8 8 7 7 6 6 5 5 4 4 3 3 2 2 1 0 } 3 | @v2 = { 5 4 3 2 1 0 } 4 | @v3 = { 0 3 5 8 10 10 8 5 3 0 } 5 | @v4 = { 12 11 10 9 8 8 8 7 6 5 4 3 2 1 0 } 6 | A @v0 c d @v1 e @v2 f @v3 g @v4 a 7 | -------------------------------------------------------------------------------- /tests/features/adsr.mmlx: -------------------------------------------------------------------------------- 1 | /** 2 | * start at 15 and go to 0 in 10 steps 3 | */ 4 | adsr1: 5 | adsr: 0 0 15 10 6 | 7 | /** 8 | * same as adsr1 but written differently 9 | */ 10 | adsr2: 11 | adsr: 0 0 0 10 12 | 13 | /** 14 | * start at 0 go to 10 in 10 frames then go to 0 in 20 frames 15 | */ 16 | adsr3: 17 | adsr: 10 10 10 20 18 | 19 | /** 20 | * start at 5 and go to 0 in 6 steps 21 | */ 22 | adsr4: 23 | adsr: 0 0 5 6 24 | 25 | /** 26 | * test with sustain at max 27 | */ 28 | adsr5: 29 | adsr: 5 0 10 5 30 | 31 | /** 32 | * test using max_volume property 33 | */ 34 | adsr6: 35 | adsr: 0 5 8 10 36 | max_volume: 12 37 | 38 | A @adsr1 c @adsr2 d @adsr3 e @adsr4 f @adsr5 g @adsr6 a 39 | -------------------------------------------------------------------------------- /tests/features/collapse-octave-shifts.mml: -------------------------------------------------------------------------------- 1 | A c d e f 2 | -------------------------------------------------------------------------------- /tests/features/collapse-octave-shifts.mmlx: -------------------------------------------------------------------------------- 1 | A c > < d >><< e <> <> f 2 | -------------------------------------------------------------------------------- /tests/features/collapse-spaces.mml: -------------------------------------------------------------------------------- 1 | A a b c d e f g a b c b a 2 | -------------------------------------------------------------------------------- /tests/features/collapse-spaces.mmlx: -------------------------------------------------------------------------------- 1 | A a b c d e f g a b c b a 2 | -------------------------------------------------------------------------------- /tests/features/comments.mml: -------------------------------------------------------------------------------- 1 | A a b c 2 | B c d e f 3 | -------------------------------------------------------------------------------- /tests/features/comments.mmlx: -------------------------------------------------------------------------------- 1 | /** 2 | * this is a test to make sure all comments get stripped 3 | */ 4 | // ones like this too 5 | ; and like this 6 | A a b c // and on the end of a line 7 | B c d e /* and in the middle of a line */ f 8 | -------------------------------------------------------------------------------- /tests/features/curves.mml: -------------------------------------------------------------------------------- 1 | @v0 = { 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 4 4 5 6 6 7 8 8 9 10 11 12 13 14 15 } 2 | @v1 = { 0 0 1 2 3 4 5 6 6 7 8 8 9 10 10 11 11 12 12 12 13 13 13 14 14 14 14 14 14 14 15 } 3 | @v2 = { 0 0 0 0 0 0 1 1 2 2 3 4 4 5 6 7 8 9 10 10 11 12 12 13 13 14 14 14 14 14 15 } 4 | @v3 = { 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 3 3 4 5 5 6 7 8 9 10 12 13 15 } 5 | @v4 = { 0 1 2 4 5 6 7 8 9 9 10 11 11 12 12 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 15 } 6 | @v5 = { 0 0 0 0 0 0 0 0 1 1 2 2 3 4 6 7 8 10 11 12 12 13 13 14 14 14 14 14 14 14 15 } 7 | @v6 = { 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 3 4 5 6 7 8 9 11 13 15 } 8 | @v7 = { 0 1 3 5 6 7 8 9 10 11 12 12 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 } 9 | @v8 = { 0 0 0 0 0 0 0 0 0 0 1 2 3 4 5 7 9 10 11 12 13 14 14 14 14 14 14 14 14 14 15 } 10 | A @v0 @v1 @v2 11 | A @v3 @v4 @v5 12 | A @v6 @v7 @v8 13 | -------------------------------------------------------------------------------- /tests/features/curves.mmlx: -------------------------------------------------------------------------------- 1 | easeInQuad: 2 | volume: 0..15.curve('easeInQuad').step(.5) 3 | 4 | easeOutQuad: 5 | volume: 0..15.curve('easeOutQuad').step(.5) 6 | 7 | easeInOutQuad: 8 | volume: 0..15.curve('easeInOutQuad').step(.5) 9 | 10 | easeInCubic: 11 | volume: 0..15.curve('easeInCubic').step(.5) 12 | 13 | easeOutCubic: 14 | volume: 0..15.curve('easeOutCubic').step(.5) 15 | 16 | easeInOutCubic: 17 | volume: 0..15.curve('easeInOutCubic').step(.5) 18 | 19 | easeInQuart: 20 | volume: 0..15.curve('easeInQuart').step(.5) 21 | 22 | easeOutQuart: 23 | volume: 0..15.curve('easeOutQuart').step(.5) 24 | 25 | easeInOutQuart: 26 | volume: 0..15.curve('easeInOutQuart').step(.5) 27 | 28 | A @easeInQuad @easeOutQuad @easeInOutQuad 29 | A @easeInCubic @easeOutCubic @easeInOutCubic 30 | A @easeInQuart @easeOutQuart @easeInOutQuart 31 | -------------------------------------------------------------------------------- /tests/features/import.mml: -------------------------------------------------------------------------------- 1 | A c1 2 | B e1 3 | C a1 4 | -------------------------------------------------------------------------------- /tests/features/import.mmlx: -------------------------------------------------------------------------------- 1 | @import "_import1" 2 | @import "_import2" 3 | -------------------------------------------------------------------------------- /tests/features/instrument.mml: -------------------------------------------------------------------------------- 1 | @0 = { 2 } 2 | @v0 = { 15 } 3 | A o3 @@0 @v0 q8 c 4 | -------------------------------------------------------------------------------- /tests/features/instrument.mmlx: -------------------------------------------------------------------------------- 1 | 2 | square-test: 3 | timbre: 2 4 | volume: 15 5 | q: 8 6 | 7 | A o3 @square-test c 8 | -------------------------------------------------------------------------------- /tests/features/magic-macro.mml: -------------------------------------------------------------------------------- 1 | @v0 = { 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 } 2 | @v1 = { 0 0 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 9 10 11 12 } 3 | @v2 = { 15 14 13 12 11 10 9 8 7 6 5 4 3 3 3 4 5 6 7 7 7 6 5 4 3 2 1 0 } 4 | @v3 = { 15 14 14 13 13 12 12 11 11 10 10 9 9 8 8 7 7 6 6 5 5 4 4 3 3 2 2 1 1 0 0 } 5 | A @v0 @v1 @v2 @v3 6 | -------------------------------------------------------------------------------- /tests/features/magic-macro.mmlx: -------------------------------------------------------------------------------- 1 | climb-and-fall: 2 | volume: 0..15 15..0 3 | 4 | slow-climb: 5 | volume: 0 0 0 0 1(+0.5)..7 7..12 6 | 7 | all-over-the-place: 8 | volume: 15..3 3 3..7 7 7..0 9 | 10 | falling-positive: 11 | volume: 15(.5)..0 12 | 13 | falling-negative: 14 | volume: 15(-.5)..0 15 | 16 | A @climb-and-fall @slow-climb @all-over-the-place @falling-positive @falling-negative 17 | -------------------------------------------------------------------------------- /tests/features/repeat.mml: -------------------------------------------------------------------------------- 1 | @v0 = { 1 3 5 7 9 11 13 15 1 3 5 7 9 11 13 15 } 2 | A @v0 3 | -------------------------------------------------------------------------------- /tests/features/repeat.mmlx: -------------------------------------------------------------------------------- 1 | /** 2 | * so many ways to say the same thing 3 | */ 4 | repeat1: 5 | volume: 1..15.step(2) 1..15.step(2) 6 | 7 | repeat2: 8 | volume: 1(+2)..15 1(+2)..15 9 | 10 | repeat3: 11 | volume: 1..15.step(2).repeat(2) 12 | 13 | repeat4: 14 | volume: [1..15].step(2).repeat(2) 15 | 16 | repeat5: 17 | volume: 1(+2)..15.repeat(2) 18 | 19 | repeat6: 20 | volume: [1..15.step(2)].repeat(2) 21 | 22 | repeat7: 23 | volume: [1(+2)..15].repeat(2) 24 | 25 | repeat8: 26 | volume: [[1..15].step(2)].repeat(2) 27 | 28 | repeat9: 29 | volume: [1..15].step(2) [1..15].step(2) 30 | 31 | repeat10: 32 | volume: 1 3 5 7 9 11 13 15 [1..15].step(2) 33 | 34 | A @repeat1 @repeat2 @repeat3 @repeat4 @repeat5 @repeat6 @repeat7 @repeat8 @repeat9 @repeat10 35 | -------------------------------------------------------------------------------- /tests/features/slide.mml: -------------------------------------------------------------------------------- 1 | @0 = { 2 } 2 | @v0 = { 10 } 3 | @EP0 = { 50 51 51 51 51 51 51 0 } 4 | @EP1 = { 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 0 } 5 | @EP2 = { 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 0 } 6 | @EP3 = { 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 0 } 7 | @EP4 = { 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 0 } 8 | A o4 @@0 @v0 c1 EP0 c1 EPOF >> 9 | A o4 c1 EP0 c1 EPOF >> 10 | A o4 c1 EP1 c1 EPOF >> 11 | A o4 c1 EP2 c1 EPOF >> 12 | A o4 c1 EP3 c1 EPOF >> 13 | A o4 c1^1 EP4 c1^1 EPOF >> 14 | -------------------------------------------------------------------------------- /tests/features/slide.mmlx: -------------------------------------------------------------------------------- 1 | square: 2 | timbre: 2 3 | volume: 10 4 | 5 | // sixteenth note slide 6 | A o4 @square c1 /16 > > g1 7 | A o4 @square c1 / > > g1 8 | 9 | // eigth note slide 10 | A o4 @square c1 /8 > > g1 11 | 12 | // quarter note slide 13 | A o4 @square c1 /4 > > g1 14 | 15 | // half note slide 16 | A o4 @square c1 /2 > > g1 17 | 18 | // whole note slide 19 | A o4 @square c1^1 /1 > > g1^1 20 | -------------------------------------------------------------------------------- /tests/features/variables.mml: -------------------------------------------------------------------------------- 1 | A a c e > a 2 | A [c e g > c]5 3 | -------------------------------------------------------------------------------- /tests/features/variables.mmlx: -------------------------------------------------------------------------------- 1 | test_var = a c e > a 2 | test_var2 = c e g > c 3 | A test_var 4 | A [test_var2]5 5 | -------------------------------------------------------------------------------- /tests/run_tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os, unittest, sys, inspect, glob 4 | 5 | cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0] + '/../mmlxlib' 6 | if cmd_folder not in sys.path: 7 | sys.path.insert(0, cmd_folder) 8 | 9 | from instrument import Instrument 10 | from warpwhistle import WarpWhistle 11 | 12 | class InstrumentTest(unittest.TestCase): 13 | 14 | def testHasParent(self): 15 | instrument = Instrument({ 16 | 'volume': '8 7 6 5 4', 17 | 'q': '4', 18 | 'extends': 'other' 19 | }) 20 | self.assertTrue(instrument.hasParent()) 21 | 22 | instrument = Instrument({ 23 | 'volume': '8 7 6 5 4', 24 | 'timbre': '0' 25 | }) 26 | self.assertFalse(instrument.hasParent()) 27 | 28 | def testInherit(self): 29 | other_other_instrument = Instrument({ 30 | 'timbre': '0 0 2' 31 | }) 32 | 33 | other_instrument = Instrument({ 34 | 'volume': '10 9 8 7 6', 35 | 'q': '4', 36 | 'extends': 'other_other' 37 | }) 38 | 39 | instrument = Instrument({ 40 | 'volume': '8 7 6 5 4', 41 | 'extends': 'other' 42 | }) 43 | 44 | self.assertEqual(instrument.extends, 'other') 45 | self.assertFalse(hasattr(instrument, 'q')) 46 | self.assertFalse(hasattr(instrument, 'timbre')) 47 | 48 | instrument.inherit(other_instrument) 49 | 50 | self.assertEqual(instrument.extends, 'other_other') 51 | self.assertEqual(instrument.volume, '8 7 6 5 4') 52 | self.assertEqual(instrument.q, '4') 53 | self.assertFalse(hasattr(instrument, 'timbre')) 54 | 55 | other_instrument.inherit(other_other_instrument) 56 | 57 | instrument.inherit(other_instrument) 58 | 59 | self.assertFalse(hasattr(instrument, 'extends')) 60 | self.assertEqual(instrument.volume, '8 7 6 5 4') 61 | self.assertEqual(instrument.q, '4') 62 | self.assertEqual(instrument.timbre, '0 0 2') 63 | 64 | class Logger(object): 65 | BLUE = 'blue' 66 | LIGHT_BLUE = 'light_blue' 67 | PINK = 'pink' 68 | YELLOW = 'yellow' 69 | WHITE = 'white' 70 | GREEN = 'green' 71 | RED = 'red' 72 | GRAY = 'gray' 73 | UNDERLINE = 'underline' 74 | ITALIC = 'italic' 75 | 76 | def color(self, message, color, bold = False): 77 | pass 78 | 79 | def log(self, message, verbose_only=False): 80 | pass 81 | 82 | class MMLXTest(unittest.TestCase): 83 | def removeWhitespace(self, content): 84 | lines = content.splitlines() 85 | new_lines = [] 86 | for line in lines: 87 | new_lines.append(line.strip()) 88 | 89 | string = "\n".join(new_lines) 90 | 91 | # remove any new lines at beginning or end of string 92 | return string.strip() 93 | 94 | def getContents(self, path): 95 | file = open(path, "r") 96 | content = file.read() 97 | file.close() 98 | 99 | return content 100 | 101 | def testFeatures(self): 102 | self.runForDirectory('features') 103 | 104 | def testChips(self): 105 | self.runForDirectory('chips') 106 | 107 | def testSongs(self): 108 | self.runForDirectory('songs') 109 | 110 | def runForDirectory(self, directory): 111 | data = os.path.join(os.path.dirname(__file__), directory) 112 | mmlx_files = glob.glob(data + '/*.mmlx') 113 | for mmlx_file in mmlx_files: 114 | if os.path.basename(mmlx_file).startswith('_'): 115 | continue 116 | 117 | content = self.getContents(mmlx_file) 118 | 119 | whistle = WarpWhistle(content, Logger(), {}) 120 | whistle.import_directory = os.path.dirname(mmlx_file) 121 | 122 | mml_file_name = mmlx_file.replace('.mmlx', '.mml') 123 | 124 | self.assertTrue(os.path.exists(mml_file_name), "\n\nFile does not exist at: " + mml_file_name) 125 | 126 | mml_content = self.removeWhitespace(self.getContents(mml_file_name)) 127 | 128 | mml = self.removeWhitespace(whistle.play()[0]) 129 | 130 | self.assertEqual(mml_content, mml, "\n\nFailure in test: " + mmlx_file + '\nExpected: \'' + mml_content.replace('\n', '\\n') + '\'' + '\nActual: \'' + mml.replace('\n', '\\n') + '\'') 131 | 132 | if __name__ == '__main__': 133 | unittest.main() 134 | -------------------------------------------------------------------------------- /tests/songs/demo3.mml: -------------------------------------------------------------------------------- 1 | #TITLE Demo 3 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | @20 = { 1 } 5 | @21 = { 2 } 6 | @v20 = { 10 } 7 | @v21 = { 12 } 8 | @EP20 = { 23 23 24 24 24 24 0 } 9 | @EP21 = { 21 21 21 21 21 22 0 } 10 | @EP22 = { -16 -16 -16 -16 -16 -16 0 } 11 | @EN20 = { 0 0 4 0 0 3 0 0 5 0 0 } 12 | @EN21 = { 0 0 3 0 0 4 0 0 5 0 0 } 13 | ABCDE t150 14 | @v1 = { 5 } 15 | A o4 SD0 @vr1 [@@20 @v20 q6 c c @@21 @v21 EN20 q4 c ENOF @@20 @v20 q6 EP20 c EPOF d d @@21 @v21 EN20 q4 d ENOF @@20 @v20 q6 EP21 d EPOF g g @@21 @v21 EN20 q4 g ENOF @@20 @v20 q6 EP22 g EPOF e e @@21 @v21 EN21 q4 e ENOF @@20 @v20 q6 e]2 SDOF 16 | C o4 [c c e8 e8 g8 g8 d d f+8 f+8 a8 a8 g g b8 b8 d8 d8 e e g8 g8 b8 b8]2 17 | -------------------------------------------------------------------------------- /tests/songs/demo3.mmlx: -------------------------------------------------------------------------------- 1 | #TITLE Demo 3 2 | #COMPOSER Craig Campbell 3 | #PROGRAMER Craig Campbell 4 | ABCDE t150 5 | 6 | // start the counter at 20 since we have already used v1 7 | #X-COUNTER 20 8 | 9 | /** 10 | * instruments 11 | */ 12 | square: 13 | timbre: 1 14 | volume: 10 15 | q: 6 16 | 17 | chord: 18 | timbre: 2 19 | volume: 12 20 | q: 4 21 | 22 | major: 23 | @extends "chord" 24 | arpeggio: 0 0 4 0 0 3 0 0 5 0 0 25 | 26 | minor: 27 | @extends "chord" 28 | arpeggio: 0 0 3 0 0 4 0 0 5 0 0 29 | 30 | // volume for self delay 31 | @v1 = { 5 } 32 | 33 | A o4 SD0 @vr1 [@square c c @major c / @square g d d @major d / @square a g g @major g / @square d e e @minor e @square e]2 SDOF 34 | C o4 [c c e8 e8 g8 g8 d d f+8 f+8 a8 a8 g g b8 b8 d8 d8 e e g8 g8 b8 b8]2 35 | -------------------------------------------------------------------------------- /tests/songs/my-first-chiptune.mml: -------------------------------------------------------------------------------- 1 | #TITLE My First Chiptune 2 | #COMPOSER Your Name 3 | #PROGRAMER Your Name 4 | @0 = { 2 } 5 | @1 = { 0 } 6 | @v0 = { 15 15 14 14 13 13 12 12 11 10 10 10 10 9 9 9 8 8 8 7 7 7 6 6 6 5 5 5 4 4 4 3 3 3 2 2 2 1 1 0 } 7 | @v1 = { 12 12 11 11 10 10 9 9 8 7 7 7 7 7 7 6 6 6 6 5 5 5 5 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 0 } 8 | @v2 = { 10 8 6 4 2 0 } 9 | @MP0 = { 2 4 4 } 10 | ABCD t110 11 | A o4 @@0 @v0 < a+ > c d d+ f g a > c < a+1 > 12 | B o3 @@1 @v1 d d+ f g a a+ > c d+ d1 13 | C o4 MP0 < a+1 > f1 < a+2 > 14 | D @v2 q4 e [f16]4 f8 e f8 f f f8 e8 [f32]8 f 15 | -------------------------------------------------------------------------------- /tests/songs/my-first-chiptune.mmlx: -------------------------------------------------------------------------------- 1 | #TITLE My First Chiptune 2 | #COMPOSER Your Name 3 | #PROGRAMER Your Name 4 | 5 | /** 6 | * MMLX Variables 7 | */ 8 | #X-TRANSPOSE -2 9 | 10 | /** 11 | * Instruments 12 | */ 13 | square-wave-piano: 14 | adsr: 0 10 10 30 15 | timbre: 2 16 | 17 | guitar: 18 | max_volume: 12 19 | adsr: 0 10 7 30 20 | timbre: 0 21 | 22 | bass: 23 | vibrato: 2 4 4 24 | 25 | drum: 26 | volume: 10 8 6 4 2 0 27 | q: 4 28 | 29 | ABCD t110 30 | 31 | // pulse wave channel 32 | A o4 @square-wave-piano c d e f g a b > d c1 33 | 34 | // pulse wave channel 35 | B o3 @guitar e f g a b > c d f e1 36 | 37 | // triangle wave channel 38 | C o4 @bass c1 g1 c2 39 | 40 | // noise channel 41 | D @drum e [f16]4 f8 e f8 f f f8 e8 [f32]8 f 42 | --------------------------------------------------------------------------------