├── .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 |
--------------------------------------------------------------------------------