├── .ruby-version ├── .gitignore ├── .ruby-gemset ├── Gemfile ├── scale_filter_note.rb ├── scale_filter_generator ├── scale_filter_note_interval.rb ├── Gemfile.lock ├── LICENSE ├── scale_filter_generator.rb ├── README.md └── scale_filter_device.rb /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.6.3 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | output 3 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | scale_filter_generator 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # A sample Gemfile 3 | source "https://rubygems.org" 4 | 5 | gem "rb-music-theory", github: "chrisbratlien/rb-music-theory" 6 | -------------------------------------------------------------------------------- /scale_filter_note.rb: -------------------------------------------------------------------------------- 1 | require './scale_filter_note_interval' 2 | 3 | class ScaleFilterNote < Note 4 | def general_minor_scale 5 | intervals = ScaleFilterNoteInterval.general_minor_set 6 | Scale.new(self, intervals) 7 | end 8 | end -------------------------------------------------------------------------------- /scale_filter_generator: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | Bundler.setup(:default) 6 | 7 | require 'rb-music-theory' 8 | require './scale_filter_generator' 9 | 10 | ScaleFilterGenerator.generate_all 11 | -------------------------------------------------------------------------------- /scale_filter_note_interval.rb: -------------------------------------------------------------------------------- 1 | class ScaleFilterNoteInterval < NoteInterval 2 | def self.general_minor_set 3 | sets = NoteInterval::SCALE_SETS 4 | set = sets[:aeolian] | sets[:harmonic_minor] | sets[:melodic_minor] 5 | set.map { |n| NoteInterval.new(n) } 6 | end 7 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/chrisbratlien/rb-music-theory.git 3 | revision: f5aab74ecc666600f317dd074214c3e6ff220c57 4 | specs: 5 | rb-music-theory (0.1.1) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | 11 | PLATFORMS 12 | ruby 13 | 14 | DEPENDENCIES 15 | rb-music-theory! 16 | 17 | BUNDLED WITH 18 | 1.17.2 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Michael Hines 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scale_filter_generator.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'zlib' 3 | require 'shellwords' 4 | require './scale_filter_note' 5 | require './scale_filter_device' 6 | 7 | class ScaleFilterGenerator 8 | SCALE_NAMES = { 9 | major: "Major (Ionian)", 10 | natural_minor: "Natural Minor (Aeolian)", 11 | general_minor: "General Minor (Aeolian+Harmonic+Melodic)", 12 | dorian: "Dorian", 13 | phrygian: "Phrygian", 14 | lydian: "Lydian", 15 | mixolydian: "Mixolydian", 16 | locrian: "Locrian", 17 | harmonic_minor: "Harmonic Minor", 18 | melodic_minor: "Melodic Minor", 19 | whole_tone: "Whole Tone", 20 | diminished: "Diminished", 21 | major_pentatonic: "Major Pentatonic", 22 | minor_pentatonic: "Minor Pentatonic", 23 | minor_major_pentatonic: "Minor Major Pentatonic", 24 | enigmatic: "Enigmatic", 25 | major_neapolitan: "Major Neapolitan", 26 | minor_neapolitan: "Minor Neapolitan", 27 | minor_hungarian: "Minor Hungarian" 28 | } 29 | 30 | class << self 31 | def flat_note_name(note) 32 | ScaleFilterNote.flat_twelve_tones[note.value % 12] 33 | end 34 | 35 | def note_label(note_number) 36 | note = ScaleFilterNote.new(note_number) 37 | octave = (note_number/12) - 2 38 | "\u2666 #{note.name}#{octave}" 39 | end 40 | 41 | def generate_filter(path, filter_name, scale) 42 | Zlib::GzipWriter.open(File.join(path, "#{filter_name}.adg")) do |gz| 43 | notes_and_values = [] 44 | scale_note_values = scale.note_values.map { |note_number| note_number % 12} 45 | 127.times.map do |note_number| 46 | notes_and_values << [note_label(note_number), note_number] if scale_note_values.include?(note_number % 12) 47 | end 48 | gz.write ScaleFilterDevice.new(filter_name, notes_and_values).xml 49 | end 50 | end 51 | 52 | def generate_all 53 | FileUtils.rm_rf("output") 54 | SCALE_NAMES.each do |scale_identifier, scale_name| 55 | path = "./output/Scale Filters/#{scale_name}" 56 | FileUtils.mkdir_p(path) 57 | 12.times do |note_number| 58 | root_note = ScaleFilterNote.new(note_number) 59 | scale = root_note.send("#{scale_identifier}_scale") 60 | generate_filter(path, "#{root_note.name} #{scale_name} Scale Filter", scale) 61 | generate_filter(path, "#{flat_note_name(root_note)} #{scale_name} Scale Filter", scale) unless root_note.name == flat_note_name(root_note) 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Scale Filter MIDI Effect Devices for Ableton 4 | 5 | This is a script that generates Ableton MIDI Effect Devices that allow you to filter the piano roll in various keys and modes. 6 | 7 | ## [Download the *Scale.Filters.zip* here](https://github.com/michaelphines/scale-filter-midi-effect-device/releases) 8 | 9 |
10 | 11 | ## Demo 12 | 13 | ![demo of adding the device, clicking a note on the piano roll without a bullet to highlight a non-diatonic note, and moving it](https://s3.amazonaws.com/michaelphines-shared-files/Scale+Filters.gif) 14 | 15 | 16 | ## Quirks 17 | 18 | - Only tested on Live 9.6.2. 19 | - If the device is on, you won't be able to play non-diatonic notes. However folding and note names work just fine when the device is disabled. I can't figure out how to get it to start disabled by default. 20 | - It generates lots of theoretical scales like Fb major, but names the notes like A and C# instead of Bbb and Db. This is what Ableton does anyway, though. 21 | - If you do have non-diatonic notes in your MIDI already, those will not disappear. 22 | - I added a marker before each of the diatonic note names, so you can distinguish them. This has the unintended, but possibly pleasant side effect of making notes in the piano roll also have a marker when they're diatonic, but this only works when folded. 23 | - It's mostly untested, but *please do* report bugs! 24 | 25 | ## Scales generated: 26 | 27 | #### In all keys including theoretical ones like Fb 28 | 29 | - Diminished 30 | - Dorian 31 | - Enigmatic 32 | - Harmonic Minor 33 | - Locrian 34 | - Lydian 35 | - Major (Ionian) 36 | - Major Neapolitan 37 | - Major Pentatonic 38 | - Melodic Minor 39 | - Minor Hungarian 40 | - Minor Major Pentatonic 41 | - Minor Neapolitan 42 | - Minor Pentatonic 43 | - Mixolydian 44 | - Natural Minor (Aeolian) 45 | - Phrygian 46 | - Whole Tone 47 | 48 | - General Minor 49 | - This is a scale that adds the harmonic and melodic scale notes to the natural minor scale 50 | 51 | 52 | ## Implementation details 53 | 54 | I didn't have the patience to do it by hand, so I decided to try to write a script. A quick peak at the file header of the Ableton device revealed it was just a gziped XML file, and I found a Ruby gem that generates scales, so I was off to the races. It didn't work at first, but it appears that Ableton cares strangely a lot about the tabbing and whitespace in the XML, which frankly terrifies me. I hope I'm wrong about that. 55 | 56 | ## About me 57 | 58 | I produce music as [Archange7](https://archange7.com), you can find me on [Spotify](https://open.spotify.com/artist/53wzdEShKkG9TMGwVc0iEe) and [iTunes](https://geo.music.apple.com/us/artist/archange7/1207668748?mt=1&app=music). 59 | 60 | . -------------------------------------------------------------------------------- /scale_filter_device.rb: -------------------------------------------------------------------------------- 1 | class ScaleFilterDevice 2 | attr_accessor :device_name, :note_names_and_numbers 3 | 4 | def initialize(device_name, note_names_and_numbers) 5 | @device_name = device_name 6 | @note_names_and_numbers = note_names_and_numbers 7 | end 8 | 9 | def xml 10 | preset_xml 11 | end 12 | 13 | private 14 | 15 | def midi_effect_branch_xml(note_name, note_number) 16 | <<-MIDI_EFFECT_BRANCH_XML.chomp 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | MIDI_EFFECT_BRANCH_XML 151 | end 152 | 153 | def preset_xml 154 | <<-PRESET_XML 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 000000000256000200000C4D6163696E746F7368204844000000000000000000000000000000CFD3 199 | E476482B000001FBF7E20F474D204472756D204D61702E6164670000000000000000000000000000 200 | 0000000000000000000000000000000000000000000000000000000000000000000001FC1636D269 201 | 1A7500000000612D6C76FFFFFFFF0000092000000000000000000000000000000005546F6F6C7300 202 | 001000080000CFD42AC60000001100080000D2696ED500000001002801FBF7E201FBF7DC01FBF7D6 203 | 01FBF5D601FBF5AC006B2577005F963C005F963000093778000266F9000200974D6163696E746F73 204 | 682048443A55736572733A006D69636861656C68696E65733A004D757369633A0041626C65746F6E 205 | 3A00466163746F7279205061636B733A004C6976652038204C6567616379204C6962726172793A00 206 | 507265736574733A004D49444920456666656374733A004D49444920456666656374205261636B3A 207 | 00546F6F6C733A00474D204472756D204D61702E61646700000E0020000F0047004D002000440072 208 | 0075006D0020004D00610070002E006100640067000F001A000C004D006100630069006E0074006F 209 | 007300680020004800440012008055736572732F6D69636861656C68696E65732F4D757369632F41 210 | 626C65746F6E2F466163746F7279205061636B732F4C6976652038204C6567616379204C69627261 211 | 72792F507265736574732F4D49444920456666656374732F4D49444920456666656374205261636B 212 | 2F546F6F6C732F474D204472756D204D61702E616467001300012F00001500020013FFFF0000 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 000000000256000200000C4D6163696E746F7368204844000000000000000000000000000000CFD3 511 | E476482B000001FBF7E20F474D204472756D204D61702E6164670000000000000000000000000000 512 | 0000000000000000000000000000000000000000000000000000000000000000000001FC1636D269 513 | 1A7500000000612D6C76FFFFFFFF0000092000000000000000000000000000000005546F6F6C7300 514 | 001000080000CFD42AC60000001100080000D2696ED500000001002801FBF7E201FBF7DC01FBF7D6 515 | 01FBF5D601FBF5AC006B2577005F963C005F963000093778000266F9000200974D6163696E746F73 516 | 682048443A55736572733A006D69636861656C68696E65733A004D757369633A0041626C65746F6E 517 | 3A00466163746F7279205061636B733A004C6976652038204C6567616379204C6962726172793A00 518 | 507265736574733A004D49444920456666656374733A004D49444920456666656374205261636B3A 519 | 00546F6F6C733A00474D204472756D204D61702E61646700000E0020000F0047004D002000440072 520 | 0075006D0020004D00610070002E006100640067000F001A000C004D006100630069006E0074006F 521 | 007300680020004800440012008055736572732F6D69636861656C68696E65732F4D757369632F41 522 | 626C65746F6E2F466163746F7279205061636B732F4C6976652038204C6567616379204C69627261 523 | 72792F507265736574732F4D49444920456666656374732F4D49444920456666656374205261636B 524 | 2F546F6F6C732F474D204472756D204D61702E616467001300012F00001500020013FFFF0000 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | #{note_names_and_numbers.map { |name, number| midi_effect_branch_xml(name, number) }.join("\n")} 540 | 541 | 542 | 543 | PRESET_XML 544 | end 545 | end 546 | --------------------------------------------------------------------------------