├── .gitignore ├── .pylintrc ├── LICENSE ├── MANIFEST.in ├── README.md ├── examples ├── 1.txbt ├── 2.txbt ├── 3.txbt ├── 4.txbt ├── 5.txbt ├── 6.txbt ├── 7.txbt ├── drums1.txbt ├── jazz.txbt ├── mary.txbt └── metronome.txbt ├── requirements.txt ├── setup.py ├── test ├── arp.mid ├── arp.txbt ├── arp2.txbt ├── auto.txbt ├── cc.txbt ├── files.txbt ├── inversions.txbt ├── markers.txbt ├── modes.txbt ├── new.txbt ├── octave.txbt ├── piano.txbt ├── run.txbt ├── run2.txbt ├── scale.txbt ├── softpiano.txbt ├── strum.txbt ├── sync.txbt ├── tabs.txbt ├── tempo.txbt ├── tracks.txbt ├── tuplet.txbt ├── tuplet2.txbt └── walk.txbt ├── textbeat ├── __main__.py ├── analyzer.py ├── def │ ├── cc.yaml │ ├── dc.yaml │ ├── default.yaml │ ├── dev.yaml │ ├── exp.yaml │ ├── gm.yaml │ ├── informal.yaml │ └── style.yaml ├── defs.py ├── instrument.py ├── midi.py ├── parser.py ├── player.py ├── plugins │ ├── __init__.py │ ├── carla.py │ ├── csound.py │ ├── espeak.py │ ├── fluidsynth.py │ ├── sonicpi.py │ └── supercollider.py ├── presets │ ├── default.carxp │ ├── example.carxp │ ├── festivalrc │ └── test.xml ├── remote.py ├── run.py ├── schedule.py ├── support.py ├── theory.py ├── track.py ├── tutorial.py └── tutorial.yaml ├── txbt └── txbt.cmd /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | textbeat.egg-info/ 5 | examples/*.mid 6 | build.sh 7 | clean.sh 8 | *.old 9 | *.pyc 10 | lint.txt 11 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=C0326,C0303,R1714 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Grady O'Connell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include textbeat/def * 2 | recursive-include textbeat/presets * 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # textbeat 2 | 3 | Plaintext music sequencer and interactive shell. 4 | 5 | Write music in vim or your favorite text editor. 6 | 7 | Open-source under MIT License (see LICENSE file for information) 8 | 9 | ![Screenshot](https://i.imgur.com/HmzNhXf.png) 10 | 11 | Copyright (c) 2018 Grady O'Connell 12 | 13 | - [Project Board](https://trello.com/b/S8AsJaaA/textbeat) 14 | - Vim integration: [vim-textbeat](https://github.com/flipcoder/vim-textbeat) 15 | 16 | **This project is still very new. Despite number of features, you may quickly 17 | run into issues, especially with editor integration.** 18 | 19 | # Overview 20 | 21 | Compose music in a plaintext format or type music directly in the shell. 22 | The format is vertical and column-based, similar to early music trackers, 23 | but with syntax inspired by jazz/music theory. 24 | 25 | # Features 26 | 27 | Textbeat is still in development, but you can already do lots of cool things: 28 | 29 | - Strumming 30 | - Arpeggiation 31 | - Tuplets and polyrhythms 32 | - MIDI CC automation 33 | - Vibrato, pitch, and mod wheel control 34 | - Dynamics 35 | - Accents 36 | - Velocity 37 | - Inversions 38 | - Midi channel stacking 39 | - Note length 40 | - Delays 41 | - Scales and modes by name 42 | - Markers, repeats, callstack 43 | 44 | # Setup 45 | 46 | ## Linux 47 | 48 | ``` 49 | git clone https://github.com/flipcoder/textbeat 50 | cd textbeat 51 | sudo python setup.py install 52 | textbeat 53 | ``` 54 | 55 | ## Windows 56 | 57 | ``` 58 | git clone https://github.com/flipcoder/textbeat 59 | cd textbeat 60 | pip3 install -r requirements.txt 61 | ./txbt.cmd 62 | ``` 63 | 64 | ## Test it out! 65 | 66 | Once you're in textbeat, try this: 67 | 68 | ``` 69 | maj& 70 | ``` 71 | 72 | If you don't hear 3 notes, you need to set up midi (this is the case with Linux). 73 | 74 | ## How to set up midi 75 | 76 | You can use the shell with General Midi out-of-the-box on windows, which is great for learning, 77 | but sounds bad without a decent soundfont. 78 | 79 | If you want to use VST instruments, you'll need to route the MIDI out to something that hosts them, like a DAW. 80 | (I'm currently working on headless VST rack generation.) 81 | 82 | For windows, you can use a virtual midi driver, such as [loopMIDI](http://www.tobias-erichsen.de/software/loopmidi.html) for usage with a VST host or DAW. 83 | 84 | If you're on Linux, you can use soundfonts through qsynth or use a software instrument like helm or dexed. I recommend qsynth. 85 | 86 | VSTs should work here as well but you need to pick a host. 87 | 88 | If you feed the MIDI into a DAW you'll be able to record the output through the DAW itself. 89 | 90 | I'm currently looking into export options and recording via a headless host. 91 | 92 | # Tutorial 93 | 94 | If you're familiar with trackers, you may pick this up quite easily. 95 | 96 | First start by creating a .txbt (textbeat) file, inside the file music 97 | flows vertically, with separate columns that are separated by whitespace or 98 | manually setting a column width. 99 | 100 | Each column represents a track, which defaults to separate midi channel numbers. 101 | Tracks play sequences of notes. You'll usually play at least 1 track per instrument. 102 | This doesn't mean you're limited to just one note per track though, 103 | you can keep notes held down and play chords as you wish. 104 | 105 | Each cell row in a track can contain both note data and associated effects. 106 | 107 | By default, any note event in a track will mute previous notes on that track 108 | 109 | The following will play the C major scale using numbered notation: 110 | ``` 111 | ; Major Scale -- this is a comment, write whatever you want here! 112 | 113 | ; 120bpm subdivided into 2 (i.e., eighth notes) 114 | 115 | %t120 x2 116 | 117 | 1 118 | 2 119 | 3 120 | 4 121 | 5 122 | 6 123 | 7 124 | 1' 125 | ``` 126 | 127 | The tempo is in BPM, and the grid is based in subdivisions. 128 | Musicians can think of grid as fractions of quarter note, 129 | The grid is the beat/quarter-note subdivision. 130 | 131 | Both Tempo and Grid can be decimal numbers as well. 132 | For example, if you made some chords and you only want 133 | one chord to be played per bar (eg 4 beats) 134 | you could set `%t120x0.25`. 135 | 136 | You can listen to what you've made by running: 137 | 138 | ``` 139 | textbeat 140 | ``` 141 | 142 | Consult the output of `textbeat -h` for further information. 143 | 144 | ## Note Numbers 145 | 146 | Both note numbers and letters are supported. 147 | This tutorial will use 1,2,3,4,5,6,7 instead of C,D,E,F,G,A,B. 148 | I'm a fan of thinking about notes without implying a key. 149 | For this reason, textbeat prefers the relative/transposed note numbers 150 | over arbitrary note names. 151 | If you're writing a song in D minor, you may choose to set the global or track key 152 | to D, making D note 1. (You could also set D to 6 if you're thinking modally) 153 | If this is confusing or not beneficial to you: don't worry, it's optional! 154 | 155 | In this format, flats and sharps are prefixed instead of suffixed (b7 ("flat 7") instead of Bb ("B flat")). 156 | 157 | Be aware that this flexibility introduces a few limits with chord names: 158 | - B7 chords should not be written as 'b7', because this means flat 7 159 | - 7 is a note when used alone, not a chord: 160 | - Write it as dom7 161 | - Alternatively write 1:7, R7, or C7 162 | - 27 is not a 7 chord on 2, it's note 27 163 | - Write it as 2:7 or 2dom7 164 | 165 | ## Transposing Octaves 166 | 167 | In the first example, the apostrophe character (') was used to play the note in the next octave. 168 | For an octave below, use a comma (,). 169 | 170 | Repeat these for additional octaves (,,, for 3 down, '' for 2 up, etc). 171 | 172 | To make octave changes persist, use a number for the octave count instead of repeating (,2). 173 | 174 | ## Holding Muting 175 | 176 | Notes will continue playing automatically, until they're muted or another note is played in the same track. 177 | 178 | You can mute all notes in a track with - 179 | 180 | To control releasing of notes, use dash (-). 181 | ``` 182 | 183 | ; hold note 1 until next note 184 | 1 185 | 186 | 187 | 188 | 189 | ; auto-mute by specifying note value (*): 190 | 1* 191 | 192 | 193 | 194 | 195 | ; manually mute with '-' 196 | 1 197 | 198 | 199 | - 200 | 201 | ``` 202 | 203 | Note durations can be manually controlled by adding * to increase value by powers of two, 204 | You can also add a fractional value to multiply this. These types of fraction 205 | values are used throughout textbeat. 206 | The opposite of this is the dot (.) which halves note values 207 | 208 | 209 | ``` 210 | ; set note based on percentage (this means 30%) 211 | 1*3 212 | 213 | ; set note based on percentage (33%) 214 | 1*33 215 | 216 | ; set note based on percentage (33.3%) 217 | 1*333 218 | 219 | ; etc... 220 | ``` 221 | 222 | Now with dots for staccato: 223 | 224 | ``` 225 | 1. 226 | 227 | 1.. 228 | 229 | 1.30 230 | ``` 231 | 232 | Notes that are played in the same track as other notes mute the previous notes. 233 | In order to override this, hold a note by suffixing it with underscore (_). 234 | 235 | A (-) character will then mute them all. 236 | 237 | ``` 238 | ; Let's hold some notes 239 | 1_ 240 | 3_ 241 | 5_ 242 | 7_ 243 | - 244 | ``` 245 | 246 | If you want to hold a series of notes like a sustain pedal, simply use two underscores (__) 247 | and all future notes will be held until a mute is received. 248 | 249 | ## Chords 250 | 251 | Unlike traditional trackers, you can write chords directly: 1maj or Cmaj. 252 | 1 ('C') is not required here. as chords without note names are positioned on 1 253 | ('C') (ex. "maj" = "Cmaj" = "1maj"). 254 | Other shorthand names that work: "ma", "major", "M", or roman numeral "I" 255 | 256 | Let's play a scale with some chords: 257 | 258 | ``` 259 | %t120 g2 260 | 1maj 261 | 2m 262 | 3m 263 | 4maj 264 | 5maj 265 | 6m 266 | 7dim 267 | 1maj' 268 | ``` 269 | 270 | There are lots of chords and voicings (check def/ files) and I'll be adding a lot more. 271 | All scales and modes are usable as chords, so arpeggiation and strumming is usable with those as well. 272 | 273 | Remember: The note goes *before* the chord, so 7maj is a maj chord on note 7 (i.e. Bmaj), NOT a maj7. 274 | 275 | ## Arpeggios and Strumming 276 | 277 | Chords can be walked if they are suffixed by '&' 278 | Be sure to rest in your song long enough to hear it cycle. 279 | 280 | ``` 281 | maj& 282 | 283 | 284 | 285 | ``` 286 | 287 | After the &, you can put a number to denote the number of cycles. 288 | By default, it cycles infinitely until muted (or until the song ends) 289 | 290 | The dollar sign is similar, but walks an entire set of notes within a single grid space: 291 | ``` 292 | maj$ 293 | ``` 294 | Scales and modes are also accessible the same way: 295 | 296 | ``` 297 | dorian$ 298 | ``` 299 | 300 | To strum, use the hold (_) symbol with this. 301 | 302 | ``` 303 | maj$_ 304 | ``` 305 | 306 | ## Velocity and Accents 307 | 308 | Use a ! or ? to accent or soften a note respectively. 309 | 310 | You can use these on arpeggios as well. 311 | 312 | ``` 313 | 1! 314 | 2? 315 | 3 316 | 4? 317 | 1! 318 | 2? 319 | 3 320 | 4? 321 | ``` 322 | 323 | Use values after accent to set a specific velocity: 324 | 325 | ``` 326 | 1!! # 100% 327 | 1!90 328 | 2!75 329 | 3!5 # 50% 330 | 4!333 # 33.3% 331 | 5!05 # 5% 332 | ``` 333 | 334 | ## Note grouping 335 | 336 | For readability, notes can be indented to imply downbeat or grouping 337 | 338 | ``` 339 | 1 340 | 2 341 | 3 342 | 4 343 | 1 344 | 2 345 | 3 346 | 4 347 | ``` 348 | 349 | ## Volume 350 | 351 | Usually you'll want to control velocity through accenting('!') or softening('?') 352 | or using values (!30 for 30%) 353 | 354 | If you wish to control volume/gain directly, use @v 355 | 356 | ``` 357 | 1maj@v9 358 | - 359 | 1maj@v6 360 | - 361 | 1maj@v4 362 | - 363 | 1maj@v2 364 | - 365 | ``` 366 | 367 | Unlike accents, volume changes persist. 368 | 369 | Interpolation is not yet implemented 370 | 371 | ## Vibrato, Pitch, and Mod Wheel 372 | 373 | To add vibrato to a note, suffix it with a tilda (~). 374 | 375 | Vibrato uses the mod wheel right now, but will eventually use pitch wheel oscillation. 376 | 377 | In the future, articulation will be programmable, per-track or per-song. 378 | 379 | ## Arpeggio Modulation 380 | 381 | Notes of arpeggios can be modified as they're running, 382 | by having effects in the grid space they occur, for example: 383 | 384 | ``` 385 | maj7& 386 | .? 387 | .? 388 | .? 389 | ! 390 | .? 391 | .? 392 | .? 393 | ``` 394 | 395 | maj7& starts a repeating 4-note arpeggio, and we indent to show this. 396 | 397 | Certain notes of the sequence are modulated with short/staccato '.', soft '?', and accent '!' 398 | 399 | For staccato usage w/o a note name, an extra dot is required since '.' is simply a placeholder. 400 | 401 | ## Tracks 402 | 403 | Columns are separate tracks, line them up for more than one instrument. 404 | 405 | The dots are placeholders. 406 | 407 | ``` 408 | 1,2 1 409 | . 4 410 | . 5 411 | . 1' 412 | . 4' 413 | . 5' 414 | . 1'' 415 | ``` 416 | 417 | Columns can be detected (in some cases), but you'll probably want to 418 | specify the column width manually at the top, 419 | which allows vim to mark the columns. 420 | 421 | ``` 422 | # sets column width to 8 423 | %c=8 424 | ``` 425 | 426 | For best view in an editor, it is recommended that you offset the first column by -2: 427 | 428 | ``` 429 | # sets column width to 8, offset -2 430 | %c=8,-2 431 | ``` 432 | 433 | ## Patches 434 | 435 | Another useful global var is 'p', which sets midi patches by name or number 436 | across the tracks. The midi names support both patch numbers and partial case-insensitive 437 | matches of GM instruments. 438 | 439 | ``` 440 | %t120 x2 p=piano,guitar,bass,drums c8,-2 441 | ``` 442 | 443 | For a full list of GM names, see [def/gm.yaml](https://github.com/flipcoder/textbeat/blob/master/textbeat/def/gm.yaml). 444 | 445 | ## Tuplets 446 | 447 | The 'T' (tuplet) gives us access to the musical concept of tuplets (called triplets in cases of 3). 448 | which allows note timing and durations to fall along a ratio instead of the usual note subdivisions. 449 | 450 | Tuplets are marked by 'T' and have an optional value at the first occurrence in that group. 451 | Ratios provided will control expansion. Default is 3:4. 452 | If no denominator is given, it will default to the next power of two 453 | (so 3:4, 5:8, 7:8, 11:16). 454 | So in other words, T5 is the same as T5:8, but if you need a 5:6, you'll need to write T5:6. 455 | The ratio of the tuplet persists for the rest of the grouping. 456 | For nested tripets, group those by adding an extra 'T'. 457 | 458 | The two tracks below are a basic usage of triplets: 459 | 460 | ``` 461 | 1 1T 462 | 2 2T 463 | 3 3T 464 | 4 465 | 1 1T 466 | 2 2T 467 | 3 3T 468 | 4 469 | ``` 470 | 471 | The first column is playing notes along the grid normally, while the 472 | 2nd column is playing 3 notes in the space of the others' 4 notes. 473 | 474 | Even though there is visual spacing between the triplet groups, the 'T' value effective 475 | stretches the notes so they occur along a slower grid according to that ratio. 476 | 477 | The spaces that occur after (and between) tuplet groupings should remain empty, 478 | since they are spacers to make the expansion line up. 479 | 480 | ## Picking 481 | 482 | [Currently designing this feature](https://trello.com/c/D01rlTWp/26-picking) 483 | 484 | ## Key changes 485 | 486 | ``` 487 | # change key (this will change the key of the current scale to 3 (E)) 488 | %k=3 489 | 490 | # to set a relative key, this will go from a major scale to relative minor scale 491 | %k+6 492 | 493 | # you can also go downwards 494 | %k-6 495 | 496 | # scale names are supported, this changes the scale shape to dorian 497 | %s=dorian 498 | 499 | # you can also use mode numbers 500 | %s=2 501 | ``` 502 | 503 | ## Chords (Advanced) 504 | 505 | In textbeat, slash (/) chords do not imply inversions, 506 | but are for spanning chord voicings across octaves. Additionally, note names alone do no imply chords. 507 | For example, C/E means play a C note with an E in a lower octave, whereas a musician might 508 | interpret this as a specific chord voicing. Inversions in textbeat uses shift operator (>) instead (maj> for maj first inversion)) 509 | 510 | ``` 511 | b7maj7#4/sus2/1 512 | # same thing with note names: Bbmaj7#4/Csus2/C 513 | # suffix this with & to hear the notes walked individually 514 | ``` 515 | 516 | The above chord voicing spans 3 octaves and contains 9 notes. 517 | It is a Bbmaj7 chord w/ an added #4 (relative to Bb, which is E), followed by a lower octave Csus2. 518 | Then at the bottom, there is a C bass note. 519 | 520 | ## Examples 521 | 522 | Check out the examples/ folder. Play them with textbeat from the 523 | command line: 524 | 525 | ``` 526 | ./txbt examples/jazz.txbt 527 | ``` 528 | 529 | # Advanced 530 | 531 | ## Markers / Repeats 532 | 533 | Here are the marker/repeat commands: 534 | 535 | ``` 536 | - |: set marker 537 | - |name: set marker 'name' 538 | - :| goes back to last marker, or start 539 | - :name| goes back to last marker 'name' 540 | - :N| goes back to last marker N number of times 541 | - :name*N| goes back to last marker 'name' N number of times 542 | - || return/pop to last position after marker jump 543 | - ||| end the song here 544 | ``` 545 | 546 | ## Command line parameters (use -): 547 | 548 | ``` 549 | - (default) starts midi shell 550 | - (filename): plays file 551 | - c: play a given sequence 552 | - Passing "1 2 3 4 5" would play those note one after another 553 | - l: play a single line from the file 554 | - Not too useful yet, since it doesn't parse context 555 | - +: play range, comma-separated (+start,end) 556 | - Line numbers and marker names work 557 | - t: tempo 558 | - x: grid 559 | - n: note value 560 | - c: columns 561 | - specify width and optional shift, instead of using auto-detect 562 | - positive shift values create a "gutter" to the left 563 | - negative values eat into the size of the first column 564 | - p: set midi patches 565 | - command-separated list of patches across tracks 566 | - GM instruments names fuzzy match (Example: Piano,Organ,Flute) 567 | - --sharps: Prefer sharps 568 | - --solfege: Use solfege in output (input not yet supported) 569 | - --flats: Prefer flats (currently default) 570 | - --device=DEVICE: Set midi-device (partial match supported) 571 | ``` 572 | 573 | ## Global commands: 574 | 575 | ``` 576 | - %: set var (ex. %P=piano T=120x2 S=dorian) 577 | - K: set key/transpose 578 | - Both absolute and relative values supported 579 | - Relative values are 1-indexed using numbered note name 580 | - whole step: %k+2 whole step 581 | - half step: %k+#1 or %k+b2 582 | - invalid example (because of 1-index): %k+1 583 | - O: set global octave 584 | - R: set scale (relative) 585 | - Names and numbers supported 586 | - S: set scale (parallel) 587 | - Names and numbers supported 588 | - P: set patch(s) across channels (comma-separated) 589 | - Matches GM midi names 590 | - Supports midi patch numbers 591 | - General MIDI name matching 592 | - ;: comment 593 | - ;;: cell comment (not yet impl) 594 | 595 | To do relative values, drop the equals sign: 596 | %k-2 597 | ``` 598 | 599 | ## Track commands 600 | 601 | ``` 602 | - ': play in octave above 603 | - repeat for each additional octave (''') 604 | - for octave shift to persist, use a number instead of repeats ('3) 605 | - ,: play in octave below 606 | - number provided for octave count, default 1 (,,,) 607 | - for octave shift to persist, use a number instead of repeats (,3) 608 | - >: inversion (repeatable) 609 | - future: will be moved from track commands to chord parser 610 | - <: lower inversion (repeatable) 611 | - future: will be moved from track commands to chord parser 612 | - ~: vibrato and pitch wheel 613 | - `: mod wheel 614 | - ": repeat last cell (ignoring dots, blanks, mutes, modified repeats don't repeat) 615 | - *: set note length 616 | - defaults to one beat when used (default is hold until mute) 617 | - repeating symbol doubles note length 618 | - add a number for multiply percentage (*50) 619 | - .: half note length 620 | - halfs note value with each dot 621 | - add extra dot for using w/o note event (i.e. during arpeggiator), since lone dots dont mean anything 622 | - add a number to do multiplies (i.e. C.2) 623 | - !: accent a note (or set velocity) 624 | - set velocity by provided percentage 625 | - !! for louder notes 626 | - !! for louder accent 627 | - !! w/ number set future velocity 628 | - ?: play note quietly (or set velocity) 629 | - repeat or pass value for quieter notes 630 | - T: tuplet: triplets by default, provide ratio A:B for subdivisions 631 | - ): delay: set note delay 632 | - \: bend: (not yet implemented) 633 | - &: arpeggio: plays the given chord in a sequence 634 | - infinite sequence unless number given 635 | - more params coming soon 636 | - $: strum 637 | - plays the chord in a sequence, held by default 638 | - notes automatically fit into 1 grid beat 639 | - `: mod 640 | - ch: assign track to a midi channel 641 | - midi channels exceeding max value will be spanned across outputs 642 | - p: program assign 643 | - Set program to a given number 644 | - Global var (%) p is usually preferred for string matching 645 | - c: control change (midi CC param) 646 | - setting CC5 to 25 would be c5:25 647 | - q: play recording 648 | - Q: record 649 | - midi cc mappings 650 | - bs: bank select (not impl) 651 | - at: aftertouch 652 | - bc: breath controller 653 | - fc: foot controller 654 | - pt: portamento time 655 | - v: volume 656 | - bl: balance 657 | - pn: pan 658 | - e: expression 659 | - ga: general purpose CC 16 660 | - gb: " 17 661 | - gc: " 18 662 | - gd: " 19 663 | - sp: sustain pedal 664 | - ps: portamento switch 665 | - st: sostenuto pedal 666 | - sf: soft pedal 667 | - lg: legato pedal 668 | - hd: hold w/ release fade 669 | - o: oscillator 670 | - R: resonance 671 | - r: release 672 | - a: attack 673 | - f: filter 674 | - sa: sound ctrl 675 | - sb: " 2 676 | - sc: " 3 677 | - sd: " 4 678 | - se: " 5 679 | - pa: portmento amount 680 | - rv: reverb 681 | - tr: tremolo 682 | - cr: chorus 683 | - ph: phaser 684 | - mo: mono 685 | 686 | Track commands that start with letters should be separated 687 | from notedata by prefixing '@': 688 | Example: 1~ is fine, but 1v is not. Use 1@v You only need one to combine: 1@v5e5 689 | 690 | Note: Fractional values specified are formatted like numbers after a decimal point: 691 | Example: 3, 30, and 300 all mean 30% (read like .3, .30, etc.) 692 | 693 | CC mapping is customizable inside [def/cc.yaml](https://github.com/flipcoder/textbeat/blob/master/textbeat/def/default.yaml). 694 | 695 | ``` 696 | 697 | ## Scales, Modes, Chords, Voicings 698 | 699 | ``` 700 | # note: some of these features are not finished 701 | 702 | - < or >: inversion suffix 703 | - ex: maj> means maj 1st inversion 704 | - repeatable (ex. maj>> means 2nd inversion: 5 1' 3' or G C' E') 705 | - or specify a number (like maj>2), meaning 2nd inversion (this will be useful for scale modes later) 706 | - /: slash: layer chords across octaves (note: different from music theory interpretation) 707 | - repeat slash for multiple octaves (ex. maj//1) 708 | - add (suffix), add note to chord (ex. maj7add11) 709 | - no (suffix): remove a note by number 710 | - |: stack: combines chords/notes manually (ex. maj|sus|#11) 711 | ``` 712 | 713 | ## Defs 714 | 715 | A majority of the music index is contained in inside these files: 716 | 717 | - Default: [def/default.yaml](https://github.com/flipcoder/textbeat/blob/master/textbeatdef/default.yaml). 718 | - Informal: [def/informal.yaml](https://github.com/flipcoder/textbeat/blob/master/textbeat/def/informal.yaml). 719 | - Experimental: [def/exp.yaml](https://github.com/flipcoder/textbeat/blob/master/textbeat/def/exp.yaml). 720 | 721 | These lists does not include certain chord modifications (add, no, drop, etc.). 722 | 723 | # Plugins 724 | 725 | You may notice there are some incomplete module/plugin systems for integration 726 | with different sound outputs and instruments hosts. 727 | These plugins are not yet functional. 728 | 729 | # What else? 730 | 731 | I'm improving this faster than I'm documenting it. Because of that, not everything is explained. 732 | 733 | Check out the project board for more information on current/upcoming features. 734 | 735 | Also, check out the basic examples in the examples/ and tests/ folder. 736 | 737 | # What's the plan? 738 | 739 | Not everything is listed here because I just started this project. 740 | More to come soon! 741 | 742 | Things I'm planning on adding soon: 743 | 744 | ``` 745 | - Improved chord interpretation 746 | - MIDI input/output 747 | - MIDI stabilization 748 | - Headless VST rack integration 749 | - Csound and supercollider instrument integration 750 | - libGME for classic chiptune 751 | - Text-to-speech and singing (Espeak/Festival) 752 | ``` 753 | 754 | Features I'm adding eventually: 755 | 756 | ``` 757 | - Recording and encoding output of a project 758 | - Midi controller input and recording 759 | - Midi input chord analysis 760 | - MPE support for temperament and dynamic tonality 761 | ``` 762 | 763 | I'll be making use of python's multiprocessing or 764 | separate processes to achieve as much as I can do for timing critical stuff 765 | without doing a C++ rewrite. 766 | 767 | # Can I Help? 768 | 769 | Yes! Contact [flipcoder](https://github.com/flipcoder). 770 | 771 | -------------------------------------------------------------------------------- /examples/1.txbt: -------------------------------------------------------------------------------- 1 | %t=100x4 p=piano,bass,drums c=20,-2 2 | ,2 3 | |: 4 | maj7__@v4 1 1@v5 5 | 4 b3 6 | 5 5 7 | 1 1 8 | b3 9 | 1 1 10 | 4 5 11 | 5 b3 12 | 1 1$ 13 | b3 14 | dom4/b7, b7 1 15 | 4 b3 16 | 5 5 17 | b7, 1 18 | b3 19 | b7 1 20 | 4 5 21 | 5 b3 22 | b7, 1$ 23 | b3 24 | :| 25 | -------------------------------------------------------------------------------- /examples/2.txbt: -------------------------------------------------------------------------------- 1 | %t100 x1 p=piano,piano c16,-2 2 | ma#4/1$@v5__ 1'1 3 | ma#4/1$ 4 | ma#4/1$ 7,1 5 | ma#4/1$ 6 | ma#4/1$ 6 7 | ma#4/1$ 8 | ma#4/1$ 5 9 | ma#4/1$ 10 | 2wu/2$ 6 11 | 2wu/2$ 12 | 2wu/2$ 13 | 2wu/2$ 14 | 2wu/2$ b5 15 | 2wu/2$ 16 | 2wu/2$ 17 | 2wu/2$ 18 | %t100 x4 k3 19 | 1 1,2 20 | b3mu7 21 | b3mu7 22 | b3mu7 23 | 24 | 1 25 | b3mu7 26 | b3mu7 27 | b3mu7 28 | 29 | 1 1,1 30 | b3mu7 31 | b3mu7 32 | b3mu7 33 | 34 | 1 35 | b3mu7 36 | b3mu7 37 | b3mu7 38 | -------------------------------------------------------------------------------- /examples/3.txbt: -------------------------------------------------------------------------------- 1 | %t=120x4 p=piano,bass,drums c=20,-2 2 | 3 | ma. 3,1 1 4 | m?. 5 | m?. 6 | ma. 7 | m?. 3 8 | ma. 9 | m?. 10 | m?. 11 | ma. 1 12 | m?. 13 | m?. 14 | ma. b7 15 | m?. 3 16 | m?. 17 | ma. 18 | m?. 19 | ma. 1 20 | m?. 21 | m?. 22 | ma. 23 | m?. 3 24 | m?. 5 25 | 26 | -------------------------------------------------------------------------------- /examples/4.txbt: -------------------------------------------------------------------------------- 1 | %t100 x4 p=electric,electric c16,-2 2 | %k=3 3 | 1 ,1 4 | 2 5 | b3 6 | 4 7 | |: 8 | 5 3m!& 9 | ? 10 | ? 11 | ! 12 | ? 13 | ? 14 | ? 15 | b6 4m!&' 16 | ? 17 | ? 18 | ! 19 | ? 20 | ? 21 | ? 22 | :| 23 | |a: 24 | 6m/6==,2 25 | 6m/6.. 26 | 27 | 6m/6.. 28 | 29 | 6m/6.. 30 | :a*2| 31 | b7m 32 | 33 | b7m 34 | 35 | b7m 36 | 37 | :| 38 | 39 | -------------------------------------------------------------------------------- /examples/5.txbt: -------------------------------------------------------------------------------- 1 | %t120 x4 c20,-2 Ppiano,piano 2 | __ '1 3 | |: 4 | sus,@v5 3 5 | sus& 6 | 1 7 | 8 | sus, 2 9 | sus& 10 | 5 11 | 12 | 5sus, 3 13 | 5sus& 14 | 1 15 | 16 | 5sus, 2 17 | 5sus& 18 | 19 | 20 | :| 21 | |: 22 | 4sus, 4 23 | 4sus& 24 | 25 | 26 | 4sus, 3 27 | 4sus& 28 | 29 | 30 | 4sus, 4, 31 | 4sus& 32 | 33 | 34 | 4sus, 35 | 4sus& 36 | 37 | 38 | :| 39 | |: 40 | ma, 41 | ma& 42 | 43 | 44 | ma, 45 | ma& 46 | 47 | 48 | 5m, 49 | 5m& 50 | 51 | 52 | 5m, 53 | 5m& 54 | 55 | 56 | :| 57 | b7m, 58 | b7m& 59 | 60 | 61 | b7m, 62 | b7m& 63 | 64 | 65 | 6ma, 66 | 6ma& 67 | 68 | 69 | 6ma, 70 | 6ma& 71 | 72 | 73 | b7m, 74 | b7m& 75 | 76 | 77 | b7m, 78 | b7m& 79 | 80 | 81 | 6ma, 82 | 6ma& 83 | 84 | 85 | 6ma, 86 | 6ma& 87 | 88 | 89 | -------------------------------------------------------------------------------- /examples/6.txbt: -------------------------------------------------------------------------------- 1 | %t120x4 c24,-2 ppiano,piano 2 | '2__ 3 | |: 4 | 1/mmu7/mmu7/mmu7& 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | :| 21 | -------------------------------------------------------------------------------- /examples/7.txbt: -------------------------------------------------------------------------------- 1 | %v0 t120x2 c16,-2 ppiano,piano 2 | 3 | 4 | phyrigian 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/drums1.txbt: -------------------------------------------------------------------------------- 1 | %t120 x2 c20,-2 p=drums f=loop 2 | 1 3 | b5$? 4 | b5? 5 | 3 6 | b5$? 7 | b5? 8 | -------------------------------------------------------------------------------- /examples/jazz.txbt: -------------------------------------------------------------------------------- 1 | %x3 t180 ppiano,bass,drums c12,-2 2 | 3 | 4 | 5dom7 5,2 1 5 | - 6 | 7 | 3 8 | 9 | 1dom7 1 1 10 | 11 | 12 | 1 13 | 3 14 | - 15 | 1 16 | 4dom7 4 1 17 | - 18 | 19 | 3 20 | 21 | b7dom7, b7, 1 22 | 23 | 24 | 1 25 | 3 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/mary.txbt: -------------------------------------------------------------------------------- 1 | %ppiano,piano 2 | 3 1,2 3 | 2 4 | 1 5 | 2 6 | 3 7 | 3 8 | 3 9 | 10 | 2 11 | 2 12 | 2 13 | 14 | 3 15 | 5 16 | 5 17 | 18 | 3 19 | 2 20 | 1 21 | 2 22 | 3 23 | 3 24 | 3 25 | 3 26 | 2 27 | 2 28 | 3 29 | 2 30 | 1 31 | 32 | 1' 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /examples/metronome.txbt: -------------------------------------------------------------------------------- 1 | %n=8 p=drums c10,-2 f=loop 2 | 1 3 | b5 4 | 3 5 | b5 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pygame 2 | colorama 3 | numpy 4 | prompt_toolkit 5 | appdirs 6 | pyyaml 7 | docopt 8 | future 9 | shutilwhich 10 | mido 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from __future__ import unicode_literals 3 | from setuptools import setup, find_packages 4 | import sys 5 | if sys.version_info[0]==2: 6 | sys.exit('Sorry, python2 support is currently broken. Use python3!') 7 | setup( 8 | name='textbeat', 9 | version='0.1.0', 10 | description='text music sequencer and midi shell', 11 | url='https://github.com/filpcoder/textbeat', 12 | author='Grady O\'Connell', 13 | author_email='flipcoder@gmail.com', 14 | license='MIT', 15 | packages=['textbeat','textbeat.def','textbeat.presets','textbeat.plugins'], 16 | include_package_data=True, 17 | install_requires=[ 18 | 'pygame','colorama','prompt_toolkit','appdirs','pyyaml','docopt','future','shutilwhich','mido' 19 | ], 20 | entry_points=''' 21 | [console_scripts] 22 | textbeat=textbeat.__main__:main 23 | txbt=textbeat.__main__:main 24 | ''', 25 | zip_safe=False 26 | ) 27 | 28 | -------------------------------------------------------------------------------- /test/arp.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flipcoder/textbeat/c6de752dd3f1333308b3e45f5ad7ec95594da257/test/arp.mid -------------------------------------------------------------------------------- /test/arp.txbt: -------------------------------------------------------------------------------- 1 | ; arp once 2 | 1ma7&1 3 | . 4 | . 5 | . 6 | . 7 | . 8 | . 9 | . 10 | ; arp twice 11 | 2ma7&2 12 | . 13 | . 14 | . 15 | . 16 | . 17 | . 18 | . 19 | . 20 | . 21 | . 22 | . 23 | . 24 | . 25 | . 26 | . 27 | ; end of song, play first arp note note end 28 | 3m7& 29 | -------------------------------------------------------------------------------- /test/arp2.txbt: -------------------------------------------------------------------------------- 1 | sus/sus&:2|-1 2 | 3 | 4 | 5 | 6 | 7 | :| 8 | maj7&: 9 | -------------------------------------------------------------------------------- /test/auto.txbt: -------------------------------------------------------------------------------- 1 | %r=amsynth,helm 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/cc.txbt: -------------------------------------------------------------------------------- 1 | %f=loop c15,-2 2 | 1@bs0:0 3 | b3 4 | 5 5 | b3 6 | :| 7 | -------------------------------------------------------------------------------- /test/files.txbt: -------------------------------------------------------------------------------- 1 | ; test autoloading plugins 2 | 3 | `rack 4 | - amsynth 5 | - helm 6 | 7 | -------------------------------------------------------------------------------- /test/inversions.txbt: -------------------------------------------------------------------------------- 1 | %g.5 2 | 3 | maj7 4 | "> 5 | ">> 6 | ">>> 7 | 8 | maj7 9 | "< 10 | "<< 11 | "<<< 12 | -------------------------------------------------------------------------------- /test/markers.txbt: -------------------------------------------------------------------------------- 1 | ; marker test 2 | ; should play 1 2 1 2 3 4 4 5 6 6 6 7 1' (1') 2' 2' 2' 3 | ;:| 4 | ;|: 5 | ;:|: 6 | ;||| 7 | 1 8 | 2 9 | :| 10 | 3 11 | :a*2| 12 | 5 13 | :next|: 14 | 1' 15 | :|x: 16 | 2' 17 | :x*2| 18 | ||| 19 | |a: 20 | 4 21 | || 22 | |next: 23 | 6 24 | :2| 25 | 7 26 | || 27 | -------------------------------------------------------------------------------- /test/modes.txbt: -------------------------------------------------------------------------------- 1 | %s1 2 | 1 3 | 2 4 | 3 5 | 4 6 | 5 7 | 6 8 | 7 9 | %s2 10 | 1 11 | 2 12 | 3 13 | 4 14 | 5 15 | 6 16 | 7 17 | %k2 18 | 1 19 | 2 20 | 3 21 | 4 22 | 5 23 | 6 24 | 7 25 | -------------------------------------------------------------------------------- /test/new.txbt: -------------------------------------------------------------------------------- 1 | %t=120x4 p=bass,drums c=20,-2 2 | 3 | 1& 4 | ? 5 | ? 6 | ! 7 | ? 8 | ! 9 | ? 10 | ? 11 | ! 12 | ? 13 | ! 14 | ? 15 | m& 16 | ? 17 | ? 18 | ! 19 | ? 20 | ! 21 | ? 22 | ? 23 | ! 24 | ? 25 | ! 26 | ? 27 | 28 | -------------------------------------------------------------------------------- /test/octave.txbt: -------------------------------------------------------------------------------- 1 | 1,, 2 | 2,, 3 | 3,, 4 | 1, 5 | 2, 6 | 3, 7 | 1 8 | 2 9 | 3 10 | 1' 11 | 2' 12 | 3' 13 | 1'' 14 | 2'' 15 | 3'' 16 | 17 | ; 1,-2 18 | 2 19 | 3 20 | 1,1 21 | 2 22 | 3 23 | 1,1 24 | 2 25 | 3 26 | 1,1 27 | 2 28 | 3 29 | -------------------------------------------------------------------------------- /test/piano.txbt: -------------------------------------------------------------------------------- 1 | %t120 g.25 ppiano,piano,slap,slap 2 | 3 | __ 4 | ionian/4/4 5 | ionian/2/2 6 | ,2 7 | %g4 8 | ///2 4. 120 . 9 | 3_ 10 | 11 | 3_ 12 | 4 13 | " 3 14 | 15 | 3 16 | 17 | 3 18 | " 4 19 | 3 20 | 1 21 | 2 22 | 5, 23 | 6, 24 | ///D. 4. 25 | 3_ 26 | 27 | 3_ 28 | 4 29 | " 3 30 | 31 | 3 32 | 33 | 3 34 | " 4 35 | 3 36 | 1 37 | 2 38 | 5, 39 | =M7b5_ 6, 40 | . - 41 | . 42 | b3 43 | b3P/b3P& . 44 | . 45 | . 46 | . 47 | . 48 | . 49 | . 50 | . 51 | . 52 | . 53 | . 54 | . 55 | dorian$-_ 56 | D5/D5/D5' 57 | . 58 | . 59 | . 60 | . 61 | . 62 | . 63 | . 64 | -------------------------------------------------------------------------------- /test/run.txbt: -------------------------------------------------------------------------------- 1 | %t120x2 p=piano,drums,bass c20,-2 2 | 3 | dim$__ 1 b3,1 4 | ">> 5 | ">>>> 1 5 6 | ">> 7 | " 8 | "<< 9 | "<<<< 10 | 11 | 4dim$__ 1 12 | ">>> 13 | ">>>>>> 1 14 | ">>> 15 | " 16 | "<<< 17 | "<<<<< 18 | 19 | 5dim$__ 1 20 | ">> 21 | ">>>> 1 22 | ">> 23 | " 24 | "<< 25 | "<<<< 26 | 27 | -------------------------------------------------------------------------------- /test/run2.txbt: -------------------------------------------------------------------------------- 1 | ; thirds 2 | :3$ 3 | 2:b3$ 4 | 3:b3$ 5 | 4:3$ 6 | 5:3$ 7 | 6:b3$ 8 | 7:b3$ 9 | 1:3$' 10 | 7:b3$ 11 | 6:b3$ 12 | 5:3$ 13 | 4:3$ 14 | 3:b3$ 15 | 2:b3$ 16 | :3$ 17 | maj/1/1 18 | 19 | 20 | ; fourths 21 | :4$ 22 | 2:4$ 23 | 3:4$ 24 | 4:#4$ 25 | 5:4$ 26 | 6:4$ 27 | 7:4$ 28 | 1:4$' 29 | 7:4$ 30 | 6:4$ 31 | 5:4$ 32 | 4:#4$ 33 | 3:4$ 34 | 2:4$ 35 | :4$ 36 | sus/1/1 37 | 38 | 39 | ; fifths 40 | :5$ 41 | 2:5$ 42 | 3:5$ 43 | 4:5$ 44 | 5:5$ 45 | 6:5$ 46 | 7:b5$ 47 | 1:5$' 48 | 7:b5$ 49 | 6:5$ 50 | 5:5$ 51 | 4:5$ 52 | 3:5$ 53 | 2:5$ 54 | :5$ 55 | sus/1/1 56 | 57 | 58 | -------------------------------------------------------------------------------- /test/scale.txbt: -------------------------------------------------------------------------------- 1 | %t120 g2 2 | 1maj 3 | 2m 4 | 3m 5 | 4maj 6 | 5maj 7 | 6m 8 | 7dim 9 | 1maj' 10 | -------------------------------------------------------------------------------- /test/softpiano.txbt: -------------------------------------------------------------------------------- 1 | %g.5 2 | 3 | ; maj7, 40% vel, sustain 4 | maj7!4_ 5 | ; maj7, 1st inversion, 40% vel, sustain 6 | maj7>!4_, 7 | 8 | maj7!7 9 | maj7>!4_, 10 | 11 | 6m>,2_ 12 | 13 | /7m_-_ 14 | 15 | 6m>/6_!4-- 16 | 17 | 7m/6_!4-- 18 | 19 | %g4 20 | 21 | -------------------------------------------------------------------------------- /test/strum.txbt: -------------------------------------------------------------------------------- 1 | %p=drums,piano,piano 2 | %g4 3 | ,2 4 | 1 Cmaj$ 5 | %g3 6 | I 7 | V 8 | 1 ii 9 | %s=dorian 10 | I 11 | V 12 | 1 ii 13 | %s1 14 | I 15 | V 16 | %g2 17 | 1 18 | iio7... 19 | iio7... 20 | 4,,, 21 | 1 iio7... 22 | iio7... 23 | 4,,, 24 | %g1 25 | 1,2 26 | . 27 | . 28 | . 29 | . 30 | . 31 | . 32 | . 33 | -------------------------------------------------------------------------------- /test/sync.txbt: -------------------------------------------------------------------------------- 1 | %t=120 g=4 2 | 6,2 3 6,3 3 | 6 - 4 | 6 b3 5 | 6 - 6 | 6 3 7 | 6 b3 8 | 6 - 9 | 6 3 10 | 6 - 11 | 6 3 12 | 6 - 13 | 6 3 14 | 6 b3 15 | 6 ~ 16 | 6 . 17 | 6 . 18 | 19 | -------------------------------------------------------------------------------- /test/tabs.txbt: -------------------------------------------------------------------------------- 1 | ; tab syntax 2 | ; not yet impl 3 | ||| 4 | %c20,-2 5 | 6 | |0 | 7 | | 0 | 8 | | 0 | 9 | | 0 | 10 | | 0 | 11 | | 0| 12 | 13 | |0 | 14 | | 1 | 15 | | 2 | 16 | | 3 | 17 | | 4 | 18 | | 5| 19 | 20 | |1 | 21 | | 2 | 22 | | - 3 | 23 | | - 4 | 24 | | - 5 | 25 | | 6 | 26 | 27 | - 28 | 29 | -------------------------------------------------------------------------------- /test/tempo.txbt: -------------------------------------------------------------------------------- 1 | %g3 t180 2 | C 3 | D 4 | E 5 | %t/2 6 | C 7 | D 8 | E 9 | %t*.9 10 | C 11 | D 12 | E 13 | %t*2 14 | C 15 | D 16 | E 17 | -------------------------------------------------------------------------------- /test/tracks.txbt: -------------------------------------------------------------------------------- 1 | %p=piano,synthstrings,organ 2 | 3 | sus/1/1& 1 /1 4 | . . . 5 | . . . 6 | . . . 7 | -------------------------------------------------------------------------------- /test/tuplet.txbt: -------------------------------------------------------------------------------- 1 | %pdrums,drums 2 | 3! ,1 3 | 4 | 3! 5 | 6 | 3! 7 | 3? 8 | 3! 9 | 3? 10 | 1 3T! 11 | . 3T 12 | . 3T 13 | . 14 | 1 3T! 15 | . 3T 16 | . 3T 17 | . 18 | 1 3T! 19 | . 3T 20 | 2 3T 21 | . 22 | 1 3T! 23 | . 3T 24 | 2 3T 25 | . 26 | 1 6' 27 | . 28 | . 3T 29 | 1 3T 30 | 2 3T 31 | 1 32 | . 3 33 | 1 3 34 | 1 35 | 2 3 36 | 1 3T 37 | . 3T 38 | . 3T 39 | 1 40 | 2 3 41 | 1 3 42 | . 43 | 1 3 44 | 1 45 | 2 3 46 | 1 6' 47 | -------------------------------------------------------------------------------- /test/tuplet2.txbt: -------------------------------------------------------------------------------- 1 | ; quintuplets 5(:8) and 5:6 2 | %pdrums,drums c12,-2 3 | 4 | 2 3T5!! 5 | 2 3T 6 | 2 3T 7 | 2 3T 8 | 2 3T 9 | 2 10 | 2 11 | 2 12 | 2 3T5!! 13 | 2 3T 14 | 2 3T 15 | 2 3T 16 | 2 3T 17 | 2 18 | 2 19 | 2 20 | 2 3T5:6!! 21 | 2 3T 22 | 2 3T 23 | 2 3T 24 | 2 3T 25 | 2 26 | 2 3T5:6!! 27 | 2 3T 28 | 2 3T 29 | 2 3T 30 | 2 3T 31 | 2 32 | 2 3! 33 | -------------------------------------------------------------------------------- /test/walk.txbt: -------------------------------------------------------------------------------- 1 | 1 1,1 1,2 2 | 3 3 | 5 4 | 1 5 | 3 6 | 5 7 | maj& 1 1 8 | 9 | 10 | 11 | 12 | 13 | 1 1 1 14 | 3 15 | 5 16 | 1 17 | 3 18 | 5 19 | 1' 20 | -------------------------------------------------------------------------------- /textbeat/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """textbeat 3 | Copyright (c) 2018 Grady O'Connell 4 | Open-source under MIT License 5 | 6 | Examples: 7 | textbeat shell 8 | textbeat -T start tutorial 9 | textbeat song.txbt play song 10 | 11 | Usage: 12 | textbeat [--dev=] [--midi=] [--ring] [--follow] [--stdin] [--vi] [-adeftnpsrxvL] [INPUT] 13 | textbeat [+RANGE] [--dev=] [--midi=] [--ring] [--follow] [--stdin] [--vi] [-adeftnpsrxL] [INPUT] 14 | textbeat [-rhT] 15 | textbeat -c [COMMANDS ...] 16 | textbeat -l [LINE_CONTENT ...] 17 | 18 | Options: 19 | -h --help show this 20 | -v --verbose verbose 21 | -T --tutorial (STUB) tutorial 22 | -t --tempo= (STUB) set tempo [default: 120] 23 | -x --grid= (STUB) set grid [default: 4] 24 | -n --note= (STUB) set grid using note value [default: 1] 25 | -s --speed= (STUB) playback speed [speed: 1.0] 26 | --dev= output device, partial match 27 | -p --patch= (STUB) default midi patch, partial match 28 | -f --flags comma-separated global flags 29 | -c execute commands sequentially 30 | -l execute commands simultaneously 31 | --stdin read entire file from stdin 32 | -r --remote (STUB) realtime remote (control through stdin/out) 33 | --ring don't mute midi on end 34 | -L --loop loop song 35 | --midi= generate midi file 36 | + play from line or maker, for range use start:end 37 | -e --edit (STUB) open file in editor 38 | --vi (STUB) shell vi mode 39 | -H --transpose transpose (in half steps) 40 | --sustain start with sustain enabled 41 | --numbers use note numbers in output 42 | --notenames use note names in output 43 | --flats prefer flats in output (default) 44 | --sharps prefer sharps in output 45 | --lint (STUB) analyze file 46 | --follow tracks file output for editors by printing newlines every line 47 | --quiet no output 48 | -a --analyze (STUB) midi input chord analyzer 49 | """ 50 | from __future__ import absolute_import, unicode_literals, print_function, generators 51 | # try: 52 | from .defs import * 53 | # except: 54 | # from .defs import * 55 | def main(): 56 | # if __name__!='__main__': 57 | # sys.exit(0) 58 | # ARGS = docopt(__doc__.replace('textbeat',os.path.basename(sys.argv[0]).lower())) 59 | ARGS = docopt(__doc__) 60 | set_args(ARGS) 61 | 62 | from . import support 63 | # from .support import * 64 | # from .support import * 65 | 66 | # style = style_from_dict({ 67 | # Token: '#ff0066', 68 | # Token.Prompt: '#00aa00', 69 | # Token.Info: '#000088', 70 | # }) 71 | colorama.init(autoreset=True) 72 | 73 | # logging.basicConfig(filename=LOG_FN,level=logging.DEBUG) 74 | 75 | player = Player() 76 | 77 | # class Marker: 78 | # def __init__(self,name,row): 79 | # self.name = name 80 | # self.line = row 81 | 82 | midifn = None 83 | 84 | for arg,val in iteritems(ARGS): 85 | if val: 86 | if arg == '--tempo': player.tempo = float(val) 87 | elif arg == '--midi': 88 | midifn = val 89 | player.midifile = mido.MidiFile() 90 | player.cansleep = False 91 | elif arg == '--grid': player.grid = float(val) 92 | elif arg == '--note': player.grid = float(val)/4.0 93 | elif arg == '--speed': player.speed = float(val) 94 | elif arg == '--verbose': player.showtext = True 95 | elif arg == '--dev': 96 | player.portname = val 97 | elif arg == '--vi': player.vimode = True 98 | elif arg == '--patch': 99 | vals = val.split(',') 100 | for i in range(len(vals)): 101 | val = vals[i] 102 | if val.isdigit(): 103 | player.tracks[i].patch(int(val)) 104 | else: 105 | player.tracks[i].patch(val) 106 | elif arg == '--sustain': player.sustain=True 107 | elif arg == '--ring': player.ring=True 108 | elif arg == '--remote': player.remote = True 109 | elif arg == '--lint': LINT = True 110 | elif arg == '--quiet': set_print(False) 111 | elif arg == '--follow': 112 | set_print(False) 113 | player.canfollow = True 114 | elif arg == '--flats': FLATS = True 115 | elif arg == '--sharps': SHARPS= True 116 | elif arg == '--edit': pass 117 | elif arg == '-l': player.cmdmode = 'l' 118 | elif arg == '-c': player.cmdmode = 'c' 119 | elif arg == '-T': player.tutorial = Tutorial(player) 120 | elif arg =='--flags': 121 | vals = val.split(',') 122 | player.add_flags(map(player.FLAGS.index, vals)) 123 | elif arg == '--loop': player.add_flags(Player.Flag.LOOP) 124 | # elif arg == '--renderman': player.renderman = True 125 | 126 | if player.cmdmode=='l': 127 | player.buf = ' '.join(ARGS['LINE_CONTENT']).split(';') # ; 128 | elif player.cmdmode=='c': 129 | player.buf = ' '.join(ARGS['COMMANDS']).split(' ') # spaces 130 | elif not player.tutorial: # mode n 131 | # if len(sys.argv)>=2: 132 | # FN = sys.argv[-1] 133 | FN = ARGS['INPUT'] 134 | from_stdin = False 135 | if FN=='-' or ARGS['--stdin']: 136 | FN = 0 # TEMP: doesn't work with py2 137 | from_stdin = True 138 | else: 139 | from_stdin = False 140 | if FN or from_stdin: 141 | # player.markers[''] = 0 # start marker 142 | with open(FN) as f: 143 | lc = 0 144 | for line in f.readlines(): 145 | if line: 146 | if line[-1] == '\n': 147 | line = line[:-1] 148 | elif len(line)>=2 and line[-2:0] == '\r\n': 149 | line = line[:-2] 150 | 151 | # if not line: 152 | # lc += 1 153 | # continue 154 | ls = line.strip() 155 | 156 | # place marker 157 | if ls.startswith(':'): 158 | bm = ls[1:] 159 | # only store INITIAL marker positions 160 | if not bm in player.markers: 161 | player.markers[bm] = lc 162 | elif ls.startswith('|') and ls.endswith(':'): 163 | bm = ls[1:-1] 164 | # only store INITIAL marker positions 165 | if not bm in player.markers: 166 | player.markers[bm] = lc 167 | 168 | lc += 1 169 | player.buf += [line] 170 | # player.rowno.append(lc) 171 | player.shell = False 172 | else: 173 | if player.cmdmode == 'n': 174 | player.cmdmode = '' 175 | player.shell = True 176 | 177 | player.interactive = player.shell or player.remote or player.tutorial 178 | 179 | pygame.midi.init() 180 | if pygame.midi.get_count()==0: 181 | error('No midi devices found.') 182 | sys.exit(1) 183 | dev = -1 184 | 185 | # if player.showtext: 186 | # for i in range(pygame.midi.get_count()): 187 | # log(pygame.midi.get_device_info(i)) 188 | 189 | DEVS = get_defs()['dev'] 190 | if player.showtext: 191 | log('MIDI Devices:') 192 | portnames = [] 193 | breakall = False 194 | firstpass = True 195 | for name in DEVS: 196 | for i in range(pygame.midi.get_count()): 197 | port = pygame.midi.get_device_info(i) 198 | portname = port[1].decode('utf-8') 199 | if port[3]!=1: 200 | continue 201 | if player.showtext: 202 | log(' '*4 + portname) 203 | if player.portname: 204 | if player.portname.lower() in portname.lower(): 205 | player.portname = portname 206 | dev = i 207 | breakall = True 208 | break 209 | else: 210 | if portname.lower().startswith(name): 211 | player.portname = portname 212 | dev = i 213 | breakall = True 214 | break 215 | if firstpass: 216 | portnames += [portname] 217 | 218 | # if port[3]==1: 219 | # continue 220 | firstpass = False 221 | if breakall: 222 | break 223 | 224 | # for i in range(pygame.midi.get_count()): 225 | # port = pygame.midi.get_device_info(i) 226 | # # if port[3]==1: 227 | # # continue 228 | # portname = port[1].decode('utf-8') 229 | # if player.showtext: 230 | # log(' '*4 + portname) 231 | # if player.portname: 232 | # if player.portname.lower() in portname.lower(): 233 | # player.portname = portname 234 | # dev = i 235 | # break 236 | # else: 237 | # for name in DEVS: 238 | # if portname.lower().startswith(name): 239 | # player.portname = portname 240 | # dev = i 241 | # break 242 | # portnames += [portname] 243 | if player.showtext: 244 | log('') 245 | 246 | if dev == -1: 247 | dev = pygame.midi.get_default_output_id() 248 | 249 | player.midi += [pygame.midi.Output(dev)] 250 | player.instrument = 0 251 | player.midi[0].set_instrument(0) 252 | mch = 0 253 | for i in range(NUM_CHANNELS_PER_DEVICE): 254 | # log("%s -> %s" % (i,mch)) 255 | player.tracks.append(Track(player, i, mch)) 256 | mch += 2 if i==DRUM_CHANNEL else 1 257 | 258 | if player.sustain: 259 | player.tracks[0].sustain = player.sustain 260 | 261 | # show nice output in certain modes 262 | if player.shell or player.cmdmode in 'cl': 263 | player.showtext = True 264 | 265 | player.init() 266 | 267 | if player.shell: 268 | log(FG.BLUE + 'textbeat')# v'+str(VERSION)) 269 | log('Copyright (c) 2018 Grady O\'Connell') 270 | log('https://github.com/flipcoder/textbeat') 271 | active = support.SUPPORT_ALL & support.SUPPORT 272 | inactive = support.SUPPORT_ALL - support.SUPPORT 273 | if active: 274 | log(FG.GREEN + 'Active Modules: ' + STYLE.RESET_ALL + ', '.join(active) + STYLE.RESET_ALL) 275 | if inactive: 276 | log(FG.RED + 'Inactive Modules: ' + STYLE.RESET_ALL + ', '.join(inactive)) 277 | if player.portname: 278 | log(FG.GREEN + 'Device: ' + STYLE.RESET_ALL + '%s' % (player.portname if player.portname else 'Unknown',)) 279 | log(FG.RED + 'Other Devices: ' + STYLE.RESET_ALL + '%s' % (', '.join(portnames))) 280 | if player.portname: 281 | if player.tracks[0].midich == DRUM_CHANNEL: 282 | log(FG.GREEN + 'GM Percussion') 283 | else: 284 | log(FG.GREEN + 'GM Patch: '+ STYLE.RESET_ALL +'%s' % GM[player.tracks[0].patch_num]) 285 | 286 | # log('') 287 | # log(FG.BLUE + 'New? Type help and press enter to start the tutorial.') 288 | log('') 289 | 290 | player.run() 291 | 292 | if player.midifile: 293 | player.midifile.save(midifn) 294 | 295 | # TODO: turn all midi note off 296 | i = 0 297 | for ch in player.tracks: 298 | if not player.ring: 299 | ch.panic() 300 | ch.midi = None 301 | 302 | for mididev in player.midi: 303 | del mididev 304 | player.midi = [] 305 | pygame.midi.quit() 306 | 307 | # if __name__=='__main__': 308 | # curses.wrapper(main) 309 | 310 | support.support_stop() 311 | 312 | if __name__=='__main__': 313 | main() 314 | 315 | -------------------------------------------------------------------------------- /textbeat/analyzer.py: -------------------------------------------------------------------------------- 1 | # import * from 2 | -------------------------------------------------------------------------------- /textbeat/def/cc.yaml: -------------------------------------------------------------------------------- 1 | cc: 2 | '`': 1 # mod 3 | at: 2 # aftertouch 4 | bc: 2 # breath controller 5 | fc: 4 # foot controller 6 | pt: 5 # portamento time 7 | v: 7 # volume 8 | bl: 8 # balance 9 | pn: 10 # pan 10 | ex: 11 # expression 11 | ga: 16 # general purpose 12 | gb: 17 # " 13 | gc: 18 # " 14 | gd: 19 # " 15 | sp: 64 # sustain pedal 16 | ps: 65 # portamento switch 17 | st: 66 # sostenuto pedal 18 | sf: 67 # soft pedal 19 | lg: 68 # legato pedal 20 | hd: 69 # hold w/ release fade 21 | o: 70 # osc 22 | R: 71 # res 23 | r: 72 # release 24 | a: 73 # attack 25 | f: 74 # filter 26 | sa: 75 # sound ctrl 27 | sb: 76 # " 28 | sc: 77 # " 29 | sd: 78 # " 30 | se: 79 # " 31 | pa: 84 # portmento amount 32 | rv: 91 # reverb 33 | tr: 92 # tremolo 34 | cr: 93 # chorus 35 | ph: 94 # phaser 36 | mo: 126 # mono 37 | po: 127 # poly 38 | 39 | -------------------------------------------------------------------------------- /textbeat/def/dc.yaml: -------------------------------------------------------------------------------- 1 | chords: 2 | # experimental voicings, just for fun -- MAY BE REMOVED 3 | # warnings should be emitted to beginners using these in shell, 4 | # so they don't get confused when learning 5 | 6 | # majadd4 7 | wu: '3 4 5' 8 | wu7: '3 4 5 7' 9 | wu7b5: '2 3 b5 7' 10 | wu-: 'b3 4 5' 11 | wu-7: 'b3 4 5 b7' 12 | wu-7b5: 'b3 4 b5 b7' 13 | wwu-7: 'b3 4 5 7' 14 | wwu-7b5: 'b3 4 b5 7' 15 | 16 | # edges of scale shape along circle (i.e. darkest and brightest notes of each whole tone scale) 17 | eg: '3 4 7' # diatonic edges ("eg>>"=phyr "eg>3"=lyd "eg>6"=loc) 18 | eg-: '2 b3 7' # melodic minor edges 19 | arp: '3 4 5 7' # aka wu7, diatonic edge w/ 5, for inversions use '|5' eg: eg>|5 20 | 21 | #shorthand: 22 | # wu: 23 | # replace: ma 24 | # add: '4' 25 | # 'wu-': 26 | # replace: m 27 | # add: '4' 28 | # 'wu+': 29 | # replace: + 30 | # add: '4' 31 | 32 | chord_alts: 33 | sq: sus24 34 | melo: mmu7 # melodic minor edges w/ 5 35 | 36 | -------------------------------------------------------------------------------- /textbeat/def/default.yaml: -------------------------------------------------------------------------------- 1 | scales: 2 | diatonic: 3 | intervals: '2212221' 4 | modes: 5 | - ionian 6 | - dorian 7 | - phyrigian 8 | - lydian 9 | - mixolydian 10 | - aeolian 11 | - locrian 12 | chromatic: 13 | intervals: '11111111111' 14 | wholetone: 15 | human: 'whole tone' 16 | intervals: '222222' 17 | bebopmajor: 18 | intervals: '2212p121' 19 | pentatonic: 20 | intervals: '23223' 21 | modes: 22 | - yo 23 | - minorpentatonic 24 | - majorpentatonic 25 | - egyptian 26 | - mangong 27 | blues: 28 | intervals: '32p132' 29 | melodicminor: 30 | intervals: '2122221' 31 | modes: 32 | - melodicminor 33 | - assyrian 34 | - lydianaug 35 | - acoustic 36 | - melodicmajor 37 | - halfdim 38 | - altered 39 | harmonicminor: 40 | human: 'harmonic minor' 41 | intervals: '2122132' 42 | modes: 43 | - harmonicminor 44 | - locriann6 45 | - ionianaug 46 | - dorian#4 47 | - phyrigianminor 48 | - lydian#9 49 | - alteredbb7 50 | harmonicmajor: 51 | human: 'harmonic major' 52 | intervals: '2122132' 53 | modes: 54 | - harmonicmajor 55 | - dorianb5 56 | - phyrgianb4 57 | - lydianb3 58 | - mixolydianb2 59 | - lydianaug#2 60 | - locrianbb7 61 | doubleharmonic: 62 | human: 'double harmonic' 63 | intervals: '1312131' 64 | modes: 65 | - doubleharmonic 66 | - lydian#2#6 67 | - ultraphyrigian 68 | - hungarianminor 69 | - oriental 70 | - ionian#2#5 71 | - locrianbb3bb7 72 | neapolitan: 73 | intervals: '1222221' 74 | modes: 75 | - neapolitanmajor 76 | - leadingwholetone 77 | - lydianaugdom 78 | - minorlydian 79 | - arabian 80 | - semilocrianb4 81 | - superlocrianbb3 82 | neapolitanminor: 83 | human: 'neapolitan minor' 84 | intervals: '1222131' 85 | modes: 86 | - neapolitanminor 87 | - lydian#6 88 | - mixolydianaug 89 | - hungarian 90 | - locriandom 91 | - ionain#2 92 | - ultralocrianbb3 93 | 94 | chords: 95 | '1': '' 96 | #'#1': '#1' 97 | ':#2': '#1' 98 | ':b2': 'b2' 99 | ':2': '2' 100 | ':#2': '#2' 101 | ':b3': 'b3' 102 | ':3': '3' 103 | ':4': '4' 104 | ':#4': '#4' 105 | ':b5': 'b5' 106 | ':5': '5' 107 | ':#5': '#5' 108 | ':b6': 'b6' 109 | #':6': '6' 110 | ':#6': '#6' 111 | ':b7': 'b7' 112 | #':7': '7' 113 | ':8': '8' 114 | '2': '2' 115 | '#2': '#2' 116 | b3: 'b3' 117 | '3': '3' 118 | '4': '4' 119 | '#4': '#4' 120 | b5: 'b5' 121 | '5': '5' 122 | '#5': '#5' 123 | b6: 'b6' 124 | '#6': '#6' 125 | p7: '7' 126 | b7: 'b7' 127 | '8': '8' 128 | #9: '#9' 129 | # '#11: '#11' # easy confusion with 11 chord 130 | 131 | # Common chords and voicings 132 | 133 | ma: '3 5' 134 | mab5: '3 b5' # lyd 135 | ma#4: '3 #4 5' # lyd5 136 | ma6: '3 5 6' 137 | ma69: '3 5 6 9' 138 | ma769: '3 5 6 7 9' 139 | m69: 'b3 5 6 9' 140 | ma7: '3 5 7' 141 | ma7#4: '3 #4 5 7' # lyd75 142 | ma7b5: '3 b5 7' # lyd7 143 | ma7b9: '3 5 7 b9' 144 | ma7b13: '3 5 7 9 11 b13' 145 | ma9: '3 5 7 9' 146 | ma9b5: '3 b5 7 9' 147 | ma9+: '3 #5 7 9' 148 | ma#11: '3 b5 7 9 #11' 149 | ma11: '3 5 7 9 11' 150 | ma11b5: '3 b5 7 9 11' 151 | ma11+: '3 #5 7 9 11' 152 | ma11b13: '3 #5 7 9 11 b13' 153 | ma13: '3 5 7 9 11 13' 154 | ma13b5: '3 b5 7 9 11 13' 155 | ma13+: '3 #5 7 9 11 13' 156 | ma13#4: '3 #4 5 7 9 11 13' 157 | m: 'b3 5' 158 | m6: 'b3 5 6' 159 | m7: 'b3 5 b7' 160 | m69: 'b3 5 6 9' 161 | m769: 'b3 5 6 7 9' 162 | m7b5: 'b3 b5 b7' 163 | m7+: 'b3 #5 b7' 164 | m9: 'b3 5 b7 9' 165 | m9+: 'b3 #5 b7 9' 166 | m11: 'b3 5 b7 9 11' 167 | m11+: 'b3 #5 b7 9 11' 168 | m11b9: 'b3 5 b7 b9 11' 169 | m11b13: 'b3 5 b7 9 11 b13' 170 | m13: 'b3 5 b7 9 11 13' 171 | m13b9: '3 5 b7 b9 11 13' 172 | m13#9: '3 5 b7 #9 11 13' 173 | m13b11: '3 5 b7 #9 b11 13' 174 | m13#11: '3 5 b7 #9 #11 13' 175 | +: '3 #5' 176 | 7+: '3 #5 b7' 177 | 9+: '3 #5 b7 9' 178 | 11+: '3 #5 b7 9 11' 179 | 13+: '3 #5 b7 9 11 13' 180 | 'ma6': '3 5 6' 181 | 'ma11': '3 5 b7 9 #11' 182 | 'ma13': '3 5 7 9 11 13' 183 | 'ma15': '3 5 b7 9 #11 #15' 184 | '7': '3 5 b7' 185 | 7b5: '3 b5 b7' 186 | '69': '3 5 6 b7 9' 187 | 7+: '3 #5 b7' 188 | 7b9: '3 5 b7 b9' 189 | '9': '3 5 b7 9' 190 | '#9': '3 5 b7 #9' 191 | 9b5: '3 b5 b7 9' 192 | 9+: '3 #5 b7 9' 193 | '9#11': '3 5 b7 9 #11' 194 | '11': '3 5 b7 9 11' 195 | 11b5: '3 b5 b7 9 11' 196 | 11+: '3 #5 b7 9 11' 197 | 11b9: '3 5 b7 b9 11' 198 | '6': '3 5 6' 199 | '9': '3 5 b7 9' 200 | '11': '3 5 b7 9 #11' 201 | '13': '3 5 b7 9 11 13' 202 | 13b5: '3 b5 b7 9 11 13' 203 | 13#11: '3 5 b7 9 #11 13' 204 | 13+: '3 #5 b7 9 13' 205 | '15': '3 5 b7 9 #11 #15' 206 | dim: 'b3 b5' 207 | dim7: 'b3 b5 bb7' 208 | dim9: 'b3 b5 bb7 9' 209 | dim11: 'b3 b5 bb7 9 11' 210 | dimma7: 'b3 b5 7' 211 | sus: '4 5' 212 | sus2: '2 5' 213 | mm7: 'b3 5 7' 214 | mm9: 'b3 5 7 9' 215 | pow: '5 8' 216 | 217 | # tunings (1 = lowest) 218 | guitar: '4 b7 b10 12 15' # 1=E 219 | bass: '4 b7 b10' # 1=E 220 | bass5: '4 b7 b10 b13' # 1=B 221 | bass6: '4 b7 b10 b13 16' # 1=B 222 | 223 | tune: 224 | guitar: 'E,' 225 | bass: 'E,,' 226 | bass5: 'B,,' 227 | bass6: 'B,,' 228 | 229 | chord_alts: 230 | r: '1' 231 | aug: + 232 | aug7: '7+' 233 | ma#5: + 234 | p4: '4' 235 | p5: '5' 236 | -: m 237 | M: ma 238 | sus4: sus 239 | ma7: ma7 240 | ma9: ma9 241 | oma7: dimma7 242 | p: pow 243 | o: dim 244 | o7: dim7 245 | 7o: dim7 246 | o9: dim9 247 | 9o: dim9 248 | o11: dim11 249 | 11o: dim11 250 | 251 | -------------------------------------------------------------------------------- /textbeat/def/dev.yaml: -------------------------------------------------------------------------------- 1 | dev: 2 | - synth input port # linux qsynth 3 | - timidity port 0 4 | - loopmidi 5 | - loopbe 6 | - fluidsynth-midi 7 | - fluid-synth 8 | - bassmidi driver (port a) 9 | - microsoft midi mapper # lowest preference 10 | - zynaddsubfx 11 | - hexter 12 | - midi in 13 | - virtual raw midi # carla 14 | - midi through 15 | - virmidi 16 | - qjackctl 17 | 18 | -------------------------------------------------------------------------------- /textbeat/def/exp.yaml: -------------------------------------------------------------------------------- 1 | chords: 2 | # experimental voicings, just for fun -- MAY BE REMOVED 3 | # warnings should be emitted to beginners using these in shell, 4 | # so they don't get confused when learning 5 | 6 | # majadd4 7 | wu: '3 4 5' 8 | wu7: '3 4 5 7' 9 | wu7b5: '2 3 b5 7' 10 | wu-: 'b3 4 5' 11 | wu-7: 'b3 4 5 b7' 12 | wu-7b5: 'b3 4 b5 b7' 13 | wwu-7: 'b3 4 5 7' 14 | wwu-7b5: 'b3 4 b5 7' 15 | 16 | # edges of scale shape along circle (i.e. darkest and brightest notes of each whole tone scale) 17 | eg: '3 4 7' # diatonic edges ("eg>>"=phyr "eg>3"=lyd "eg>6"=loc) 18 | eg-: '2 b3 7' # melodic minor edges 19 | arp: '3 4 5 7' # aka wu7, diatonic edge w/ 5, for inversions use '|5' eg: eg>|5 20 | 21 | #shorthand: 22 | # wu: 23 | # replace: ma 24 | # add: '4' 25 | # 'wu-': 26 | # replace: m 27 | # add: '4' 28 | # 'wu+': 29 | # replace: + 30 | # add: '4' 31 | 32 | chord_alts: 33 | sq: sus24 34 | melo: mmu7 # melodic minor edges w/ 5 35 | 36 | -------------------------------------------------------------------------------- /textbeat/def/gm.yaml: -------------------------------------------------------------------------------- 1 | patches: 2 | - Acoustic Grand Piano 3 | - Bright Acoustic Piano 4 | - Electric Grand Piano 5 | - Honky-tonk Piano 6 | - Electric Piano 1 7 | - Electric Piano 2 8 | - Harpsichord 9 | - Clavi 10 | - Celesta 11 | - Glockenspiel 12 | - Music Box 13 | - Vibraphone 14 | - Marimba 15 | - Xylophone 16 | - Tubular Bells 17 | - Dulcimer 18 | - Drawbar Organ 19 | - Percussive Organ 20 | - Rock Organ 21 | - Church Organ 22 | - Reed Organ 23 | - Accordion 24 | - Harmonica 25 | - Tango Accordion 26 | - Acoustic Guitar (nylon) 27 | - Acoustic Guitar (steel) 28 | - Electric Guitar (jazz) 29 | - Electric Guitar (clean) 30 | - Electric Guitar (muted) 31 | - Overdriven Guitar 32 | - Distortion Guitar 33 | - Guitar harmonics 34 | - Acoustic Bass 35 | - Electric Bass (finger) 36 | - Electric Bass (pick) 37 | - Fretless Bass 38 | - Slap Bass 1 39 | - Slap Bass 2 40 | - Synth Bass 1 41 | - Synth Bass 2 42 | - Violin 43 | - Viola 44 | - Cello 45 | - Contrabass 46 | - Tremolo Strings 47 | - Pizzicato Strings 48 | - Orchestral Harp 49 | - Timpani 50 | - String Ensemble 1 51 | - String Ensemble 2 52 | - SynthStrings 1 53 | - SynthStrings 2 54 | - Choir Aahs 55 | - Voice Oohs 56 | - Synth Voice 57 | - Orchestra Hit 58 | - Trumpet 59 | - Trombone 60 | - Tuba 61 | - Muted Trumpet 62 | - French Horn 63 | - Brass Section 64 | - SynthBrass 1 65 | - SynthBrass 2 66 | - Soprano Sax 67 | - Alto Sax 68 | - Tenor Sax 69 | - Baritone Sax 70 | - Oboe 71 | - English Horn 72 | - Bassoon 73 | - Clarinet 74 | - Piccolo 75 | - Flute 76 | - Recorder 77 | - Pan Flute 78 | - Blown Bottle 79 | - Shakuhachi 80 | - Whistle 81 | - Ocarina 82 | - Lead 1 (square) 83 | - Lead 2 (sawtooth) 84 | - Lead 3 (calliope) 85 | - Lead 4 (chiff) 86 | - Lead 5 (charang) 87 | - Lead 6 (voice) 88 | - Lead 7 (fifths) 89 | - Lead 8 (bass + lead) 90 | - Pad 1 (new age) 91 | - Pad 2 (warm) 92 | - Pad 3 (polysynth) 93 | - Pad 4 (choir) 94 | - Pad 5 (bowed) 95 | - Pad 6 (metallic) 96 | - Pad 7 (halo) 97 | - Pad 8 (sweep) 98 | - FX 1 (rain) 99 | - FX 2 (soundtrack) 100 | - FX 3 (crystal) 101 | - FX 4 (atmosphere) 102 | - FX 5 (brightness) 103 | - FX 6 (goblins) 104 | - FX 7 (echoes) 105 | - FX 8 (sci-fi) 106 | - Sitar 107 | - Banjo 108 | - Shamisen 109 | - Koto 110 | - Kalimba 111 | - Bag pipe 112 | - Fiddle 113 | - Shanai 114 | - Tinkle Bell 115 | - Agogo 116 | - Steel Drums 117 | - Woodblock 118 | - Taiko Drum 119 | - Melodic Tom 120 | - Synth Drum 121 | - Reverse Cymbal 122 | - Guitar Fret Noise 123 | - Breath Noise 124 | - Seashore 125 | - Bird Tweet 126 | - Telephone Ring 127 | - Helicopter 128 | - Applause 129 | - Gunshot 130 | -------------------------------------------------------------------------------- /textbeat/def/informal.yaml: -------------------------------------------------------------------------------- 1 | chords: 2 | 3 | # add2 4 | mu: '2 3 5' 5 | mu7: '2 3 5 7' 6 | mu7#4: '2 3 #4 5 7' # lyd7 3sus2|sus2 7 | mu7b5: '2 3 b5 7' 8 | 'mu-': '2 b3 5' 9 | 'mu-7': '2 b3 5 b7' 10 | 'mu-7b5': '2 3 b5 b7' 11 | 'mmu7': '2 b3 5 7' 12 | 'mmu7#4': '2 b3 #4 5 7' 13 | 'mmu7b5': '2 b3 b5 7' 14 | 15 | # jazz sus voicings 16 | q: '4 b7' # quartal (stacked 4ths) sus voicing 17 | qt: '5 9' # quintal (stacked 5ths) sus voicing 18 | sus7: '4 5 b7' 19 | sus9: '4 5 b7 9' 20 | sus24: '2 4 5' 21 | 22 | #shorthand: 23 | # mu: 24 | # replace: ma 25 | # add: '2' 26 | 27 | chord_alts: 28 | lyd: mab5 29 | lyd7: ma7b5 30 | plyd: ma#4 31 | lyd5: ma#4 32 | lyd57: ma7#4 33 | plyd7: ma7#4 34 | lyd75: ma7#4 35 | 36 | -------------------------------------------------------------------------------- /textbeat/def/style.yaml: -------------------------------------------------------------------------------- 1 | # playstyle variables like vibrato speed and depth 2 | style: 3 | -------------------------------------------------------------------------------- /textbeat/defs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from __future__ import absolute_import, unicode_literals, print_function, generators 3 | import os, sys, time, random, itertools, signal, tempfile, traceback, socket 4 | import time, subprocess, pipes, collections 5 | from collections import OrderedDict 6 | from builtins import range, str, input 7 | from future.utils import iteritems 8 | import yaml, colorama, appdirs 9 | from docopt import docopt 10 | import mido 11 | with open(os.devnull, 'w') as devnull: 12 | # suppress pygame messages 13 | stdout = sys.stdout 14 | sys.stdout = devnull 15 | import pygame, pygame.midi 16 | sys.stdout = stdout 17 | from multiprocessing import Process,Pipe 18 | from prompt_toolkit import prompt 19 | # from prompt_toolkit.styles import style_from_dict 20 | from prompt_toolkit.history import InMemoryHistory 21 | from prompt_toolkit.history import FileHistory 22 | # from prompt_toolkit.token import Token 23 | 24 | if sys.version_info[0]==3: 25 | basestring = str 26 | 27 | # VERSION = '0.1' 28 | FG = colorama.Fore 29 | BG = colorama.Back 30 | STYLE = colorama.Style 31 | NUM_TRACKS = 15 # skip drum channel 32 | NUM_CHANNELS_PER_DEVICE = 15 # "DRUM_CHANNEL = 9 33 | DRUM_CHANNEL = 9 34 | DRUM_OCTAVE = -2 35 | CSOUND_PORT = 3489 36 | EPSILON = 0.0001 37 | ARGS = None 38 | RANGE = 109 39 | OCTAVE_BASE = 5 40 | DRUM_WORDS = ['drum','drums','drumset','drumkit','percussion'] 41 | CCHAR = ' <>=~.\'`,_&|!?*\"$(){}[]%@;' 42 | CCHAR_START = 'TV' # control chars 43 | PRINT = True 44 | 45 | def bit(x): 46 | return 1 << x 47 | def cmp(a,b): 48 | return bool(a>b) - bool(a0) - bool(a<0) 51 | def orr(a,b,bad=False): 52 | return a if (bool(a)!=bad if bad==False else a) else b 53 | def indexor(a,i,d=None): 54 | try: 55 | return a[i] 56 | except: 57 | return d 58 | class Wrapper: 59 | def __init__(self, value=None): 60 | self.value = value 61 | def __len__(self): 62 | return len(self.value) 63 | 64 | def fcmp(a, b=0.0, ep=EPSILON): 65 | v = a - b 66 | av = abs(v) 67 | if av > EPSILON: 68 | return sgn(v) 69 | return 0 70 | def feq(a, b=0.0, ep=EPSILON): 71 | return not fcmp(a,b,ep) 72 | def fzero(a,ep=EPSILON): 73 | return 0==fcmp(a,0.0,ep) 74 | def fsnap(a,b,ep=EPSILON): 75 | return a if fcmp(a,b) else b 76 | def forr(a,b,bad=False): 77 | return a if (fcmp(a) if bad==False else a) else b 78 | 79 | def set_args(args): 80 | global ARGS 81 | ARGS = args 82 | def get_args(): 83 | return ARGS 84 | def constrain(a,n1=1,n2=0): 85 | try: 86 | int(a) 87 | except ValueError: 88 | # fix defaults for float 89 | if n2==0: 90 | n2 = 0.0 91 | if n1==1: 92 | n1 = 1.0 93 | return min(max(n1,n2),max(min(n1,n2),a)) 94 | 95 | APPNAME = 'textbeat' 96 | DIR = appdirs.AppDirs(APPNAME) 97 | # LOG_FN = os.path.join(DIR.user_log_dir,'.log') 98 | HISTORY_FN = os.path.join(DIR.user_config_dir, 'history') 99 | HISTORY = FileHistory(HISTORY_FN) 100 | SCRIPT_PATH = os.path.dirname(os.path.join(__file__)) 101 | CFG_PATH = os.path.join(SCRIPT_PATH, 'config') 102 | DEF_PATH = os.path.join(SCRIPT_PATH, 'def') 103 | DEF_EXT = '.yaml' 104 | def cfg_path(): 105 | return CFG_PATH 106 | def def_path(): 107 | return DEF_PATH 108 | try: 109 | os.makedirs(DIR.user_config_dir) 110 | except OSError: 111 | pass 112 | 113 | class SignalError(BaseException): 114 | pass 115 | class ParseError(BaseException): 116 | def __init__(self, s=''): 117 | super(BaseException,self).__init__(s) 118 | def quitnow(signum,frame): 119 | raise SignalError() 120 | 121 | signal.signal(signal.SIGTERM, quitnow) 122 | signal.signal(signal.SIGINT, quitnow) 123 | 124 | class BGCMD: 125 | NONE = 0 126 | QUIT = 1 127 | SAY = 2 128 | CACHE = 2 129 | CLEAR = 3 130 | 131 | def set_print(b): 132 | global PRINT 133 | PRINT = b 134 | 135 | def out(msg): 136 | if PRINT: 137 | print(msg) 138 | def log(msg): 139 | if PRINT: 140 | print(msg) 141 | def error(msg): 142 | if PRINT: 143 | print(msg) 144 | 145 | def load_cfg(fn): 146 | with open(os.path.join(CFG_PATH, fn+'.yaml'),'r') as y: 147 | return yaml.safe_load(y) 148 | def load_def(fn): 149 | with open(os.path.join(DEF_PATH, fn+'.yaml'),'r') as y: 150 | return yaml.safe_load(y) 151 | 152 | random.seed() 153 | 154 | class Diff: 155 | NONE = 0 156 | ADD = 1 157 | REMOVE = 2 158 | UPDATE = 3 159 | 160 | def merge(a, b, overwrite=False, skip=None, diff=None, pth=None): 161 | for k,v in iteritems(b): 162 | contains = k in a 163 | if contains and isinstance(a[k], dict) and isinstance(b[k], collections.abc.Mapping): 164 | loc = (pth+[k]) if pth else None 165 | if callable(skip): 166 | if not skip(loc,v): 167 | merge(a[k],b[k], overwrite, skip, diff, loc) 168 | else: 169 | merge(a[k],b[k], overwrite, skip, diff, loc) 170 | else: 171 | if contains: 172 | if callable(overwrite): 173 | loc = (pth+[k]) if pth!=None else k 174 | if overwrite(loc,v): 175 | a[k] = b[k] 176 | if diff!=None: 177 | diff.add((Diff.UPDATE,loc,v)) 178 | else: 179 | pass 180 | elif overwrite: 181 | if diff!=None: 182 | old = copy.copy(a[k]) 183 | a[k] = b[k] 184 | if diff!=None: 185 | loc = (pth+[k]) if pth!=None else k 186 | diff.add((Diff.UPDATE,loc,v,old)) 187 | else: 188 | a[k] = b[k] 189 | if diff!=None: 190 | loc = (pth+[k]) if pth!=None else k 191 | diff.add((Diff.ADD,loc,v)) 192 | return a 193 | 194 | DEFS = {} 195 | for f in os.listdir(DEF_PATH): 196 | if f.lower().endswith(DEF_EXT): 197 | merge(DEFS,load_def(f[:-len(DEF_EXT)])) 198 | 199 | CC = {} 200 | try: 201 | CC = DEFS['cc'] 202 | except KeyError: 203 | pass 204 | 205 | def get_defs(): 206 | return DEFS 207 | 208 | from .schedule import * 209 | from .theory import * 210 | from .midi import * 211 | from .track import * 212 | from .parser import * 213 | from .remote import * 214 | from .player import * 215 | 216 | -------------------------------------------------------------------------------- /textbeat/instrument.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | class Instrument(object): 4 | def __init__(self,name): 5 | self.name = name 6 | def inited(self): 7 | return False 8 | def supported(self): 9 | return False 10 | def support(self): 11 | return [] 12 | def stop(self): 13 | pass 14 | 15 | PLUGINS = [] 16 | # # plugins call this method 17 | # def export(s): 18 | # global PLUGINS 19 | # if s not in PLUGINS: 20 | # PLUGINS.append(s()) 21 | # def plugins(): 22 | # return PLUGINS 23 | 24 | -------------------------------------------------------------------------------- /textbeat/midi.py: -------------------------------------------------------------------------------- 1 | from .defs import * 2 | MIDI_CC = 0B1011 3 | MIDI_PROGRAM = 0B1100 4 | MIDI_PITCH = 0B1110 5 | MIDI_SUSTAIN_PEDAL = 0B1000 6 | GM = get_defs()['patches'] 7 | GM_LOWER = [""]*len(GM) 8 | for i in range(len(GM)): GM_LOWER[i] = GM[i].lower() 9 | -------------------------------------------------------------------------------- /textbeat/parser.py: -------------------------------------------------------------------------------- 1 | from .defs import * 2 | 3 | def count_seq(seq, match=''): 4 | if not seq: 5 | return 0 6 | r = 0 7 | if match == '': 8 | try: 9 | match = seq[0] 10 | except IndexError: 11 | return 0 12 | for c in seq: 13 | if c != match: 14 | break 15 | r+=1 16 | return r 17 | 18 | def peel_uint(s, d=None): 19 | a,b = peel_uint_s(s,str(d) if d!=None else None) 20 | return (int(a) if a!=None and a!='' else None,b) 21 | 22 | def peel_int(s, d=None): 23 | a,b = peel_uint_s(s,str(d) if d!=None else None) 24 | return (int(a) if a!=None and a!='' else None,b) 25 | 26 | # don't cast 27 | def peel_uint_s(s, d=None): 28 | r = '' 29 | for ch in s: 30 | if ch.isdigit(): 31 | r += ch 32 | else: 33 | break 34 | if not r: return (d,0) if d!=None else ('',0) 35 | return (r,len(r)) 36 | 37 | def peel_roman_s(s, d=None): 38 | nums = 'ivx' 39 | r = '' 40 | case = -1 # -1 unknown, 0 low, 1 upper 41 | for ch in s: 42 | chl = ch.lower() 43 | chcase = (chl==ch) 44 | if chl in nums: 45 | if case > 0 and case != chcase: 46 | break # changing case ends peel 47 | r += ch 48 | chcase = 0 if (chl==ch) else 1 49 | else: 50 | break 51 | if not r: return (d,0) if d!=None else ('',0) 52 | return (r,len(r)) 53 | 54 | def peel_int_s(s, d=None): 55 | r = '' 56 | for ch in s: 57 | if ch.isdigit(): 58 | r += ch 59 | elif ch=='-' and not r: 60 | r += ch 61 | else: 62 | break 63 | if r == '-': return (0,0) 64 | if not r: return (d,'') if d!=None else (0,'') 65 | return (int(r),len(r)) 66 | 67 | def peel_float(s, d=None): 68 | r = '' 69 | decimals = 0 70 | for ch in s: 71 | if ch.isdigit(): 72 | r += ch 73 | elif ch=='-' and not r: 74 | r += ch 75 | elif ch=='.': 76 | if decimals >= 1: 77 | break 78 | r += ch 79 | decimals += 1 80 | else: 81 | break 82 | # don't parse trailing decimals 83 | if r and r[-1]=='.': r = r[:-1] 84 | if not r: return (d,0) if d!=None else (0,0) 85 | return (float(r),len(r)) 86 | 87 | def peel_any(s, match, d=''): 88 | r = '' 89 | ct = 0 90 | for ch in s: 91 | if ch in match: 92 | r += ch 93 | ct += 1 94 | else: 95 | break 96 | return (orr(r,d),ct) 97 | 98 | def note_value(s): # turns dot note values (. and *) into frac 99 | if not s: 100 | return (0.0, 0) 101 | r = 1.0 102 | dots = count_seq(s) 103 | s = s[dots:] 104 | num,ct = peel_float(s, 1.0) 105 | s = s[ct:] 106 | if s[0]=='*': 107 | if dots==1: 108 | r = num 109 | else: 110 | r = num*pow(2.0,float(dots-1)) 111 | elif s[0]=='.': 112 | num,ct = peel_int_s(s) 113 | if ct: 114 | num = int('0.' + num) 115 | else: 116 | num = 1.0 117 | s = s[ct:] 118 | r = num*pow(0.5,float(dots)) 119 | return (r, dots) 120 | 121 | -------------------------------------------------------------------------------- /textbeat/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import os 3 | plugins = os.listdir(os.path.dirname(__file__)) 4 | plugins = list(filter(lambda x: x not in ['__pycache__','__init__'], map(lambda x: x.split('.')[0], plugins))) 5 | __all__ = plugins 6 | -------------------------------------------------------------------------------- /textbeat/plugins/carla.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import textbeat.instrument as instrument 3 | from textbeat.instrument import Instrument 4 | from shutilwhich import which 5 | 6 | ERROR = False 7 | if which('carla'): 8 | ERROR = True 9 | 10 | class Carla(Instrument): 11 | NAME = 'carla' 12 | def __init__(self, args): 13 | Instrument.__init__(self, Carla.NAME) 14 | self.initialized = False 15 | self.enabled = False 16 | self.soundfonts = [] 17 | self.proc = None 18 | self.args = args 19 | self.gen_inited = False 20 | def enabled(self): 21 | return self.initialized 22 | def enable(self, rack): 23 | if not self.proc: 24 | fn = self.args['SONGNAME'] 25 | if not fn: 26 | fn = 'default' 27 | if rack: 28 | self.self.temp_proj = tempfile.mkstemp('.carxp',fn) 29 | os.close(self.temp_proj[0]) 30 | self.temp_proj = self.temp_proj[1] 31 | os.unlink(self.temp_proj) 32 | base_proj = os.path.join(os.path.join(os.path.dirname(os.path.abspath(__file__)),'presets','default.carxp')) 33 | shutil.copy2(base_proj, self.temp_proj) 34 | 35 | # add instruments to temp proj file 36 | filebuf = '' 37 | with open(self.temp_proj,'r') as f: 38 | filebuf = f.read() 39 | instrumentxml = '' 40 | i = 0 41 | for instrument in rack: 42 | fnparts = instrument.split('.') 43 | name = fnparts[0] 44 | try: 45 | ext = fnparts[1].upper() 46 | except IndexError: 47 | ext = 'LV2' 48 | instrumentxml += '\n'+\ 49 | '\n'+\ 50 | ''+ext+'\n'+\ 51 | ''+name+'\n'+\ 52 | 'x'+\ 53 | '\n'+\ 54 | '\n'+\ 55 | 'N\n'+\ 56 | 'Yes\n'+\ 57 | ''+hex(i)+'\n'+\ 58 | ''+\ 59 | '\n\n' 60 | i += 1 61 | filebuf = filebuf.replace('', ''+instrumentxml) 62 | with open(self.temp_proj,'w') as f: 63 | f.write(filebuf) 64 | 65 | self.proj = self.temp_proj 66 | self.gen_inited = True 67 | else: 68 | self.proj = fn.split('.')[0]+'.carxp' 69 | if os.path.exists(proj): 70 | log(proj) 71 | self.proc = subprocess.Popen(['carla',proj], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # '--nogui', 72 | elif not rack: 73 | log('To load a Carla project headless, create a \'%s\' file.' % proj) 74 | 75 | self.initialized = True 76 | def supported(self): 77 | return not ERROR 78 | def support(self): 79 | return ['auto','carla'] 80 | def stop(self): 81 | if self.gen_inited and self.proj: 82 | try: 83 | os.remove(carla_proj[1]) 84 | except OSError: 85 | pass 86 | except FileNotFoundError: 87 | pass 88 | 89 | if self.self.temp_proj: 90 | os.unlink(self.temp_proj) 91 | if self.self.proc: 92 | self.proc.kill() 93 | 94 | # instrument.export(FluidSynth) 95 | export = Carla 96 | 97 | -------------------------------------------------------------------------------- /textbeat/plugins/csound.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import textbeat.instrument as instrument 3 | from textbeat.instrument import Instrument 4 | from shutilwhich import which 5 | import subprocess 6 | 7 | ERROR = False 8 | if not which('csound'): 9 | ERROR = True 10 | 11 | class CSound(Instrument): 12 | NAME = 'csound' 13 | def __init__(self, args): 14 | Instrument.__init__(self, CSound.NAME) 15 | self.initialized = False 16 | self.proc = None 17 | self.csound = None 18 | def enable(self): 19 | if not initialized: 20 | self.proc = subprocess.Popen(['csound', '-odac', '--port='+str(CSOUND_PORT)], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 21 | self.csound = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 22 | self.initialized = True 23 | def enabled(self): 24 | return self.initialized 25 | def supported(self): 26 | return not ERROR 27 | def support(self): 28 | return ['csound'] 29 | def send(self, s): 30 | assert self.initialized 31 | return csound.sendto(s,('localhost',CSOUND_PORT)) 32 | # def note_on(self, t, n, v): 33 | # self.fs.noteon(t, n, v) 34 | # def note_off(self, t, n, v): 35 | # self.fs.noteoff(t, v) 36 | # pass 37 | def stop(self): 38 | self.proc.kill() 39 | pass 40 | 41 | export = CSound 42 | 43 | -------------------------------------------------------------------------------- /textbeat/plugins/espeak.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from textbeat.defs import * 3 | import textbeat.instrument as instrument 4 | from textbeat.instrument import Instrument 5 | from shutilwhich import which 6 | import subprocess 7 | 8 | ERROR = False 9 | if not which('espeak'): 10 | ERROR = True 11 | 12 | # Currently not used, caches text to speech stuff in a way compatible with jack 13 | # current super slow, need to write stabilizer first 14 | class BackgroundProcess(object): 15 | def __init__(self, con): 16 | self.con = con 17 | self.words = {} 18 | self.processes = [] 19 | def cache(self,word): 20 | try: 21 | tmp = self.words[word] 22 | except: 23 | tmp = tempfile.NamedTemporaryFile() 24 | p = subprocess.Popen(['espeak', '\"'+pipes.quote(word)+'\"','--stdout'], stdout=tmp) 25 | p.wait() 26 | self.words[tmp.name] = tmp 27 | return tmp 28 | def run(self): 29 | devnull = open(os.devnull, 'w') 30 | while True: 31 | msg = self.con.recv() 32 | # log(msg) 33 | if msg[0]==BGCMD.SAY: 34 | tmp = self.cache(msg[1]) 35 | # super slow, better option needed 36 | self.processes.append(subprocess.Popen(['mpv','-ao','jack',tmp.name],stdout=devnull,stderr=devnull)) 37 | elif msg[0]==BGCMD.CACHE: 38 | self.cache(msg[1]) 39 | elif msg[0]==BGCMD.QUIT: 40 | break 41 | elif msg[0]==BGCMD.CLEAR: 42 | self.words.clear() 43 | else: 44 | log('BAD COMMAND: ' + msg[0]) 45 | self.processes = list(filter(lambda p: p.poll()==None, self.processes)) 46 | self.con.close() 47 | for tmp in self.words: 48 | tmp.close() 49 | for proc in self.processes: 50 | proc.wait() 51 | 52 | def bgproc_run(con): 53 | self.proc = BackgroundProcess(con) 54 | self.proc.run() 55 | 56 | class ESpeak(Instrument): 57 | NAME = 'espeak' 58 | def __init__(self, args): 59 | Instrument.__init__(self, ESpeak.NAME) 60 | self.initialized = False 61 | self.proc = None 62 | self.espeak = None 63 | def enable(self): 64 | if not initialized: 65 | self.pipe, child = Pipe() 66 | self.proc = Process(target=bgproc_run, args=(child,)) 67 | self.proc.start() 68 | 69 | self.initialized = True 70 | def enabled(self): 71 | return self.initialized 72 | def supported(self): 73 | return not ERROR 74 | def support(self): 75 | return ['espeak'] 76 | # def note_on(self, t, n, v): 77 | # self.fs.noteon(t, n, v) 78 | # def note_off(self, t, n, v): 79 | # self.fs.noteoff(t, v) 80 | # pass 81 | def stop(self): 82 | if self.proc: 83 | self.pipe.send((BGCMD.QUIT,)) 84 | self.proc.join() 85 | 86 | # self.proc.kill() 87 | pass 88 | 89 | export = ESpeak 90 | 91 | -------------------------------------------------------------------------------- /textbeat/plugins/fluidsynth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import textbeat.instrument as instrument 3 | from textbeat.instrument import Instrument 4 | from shutilwhich import which 5 | 6 | ERROR = False 7 | if which('fluidsynth'): 8 | try: 9 | import fluidsynth # https://github.com/flipcoder/pyfluidsynth 10 | except: 11 | ERROR = True 12 | else: 13 | ERROR = True 14 | 15 | class FluidSynth(Instrument): 16 | NAME = 'fluidsynth' 17 | def __init__(self, args): 18 | Instrument.__init__(self, FluidSynth.NAME) 19 | self.initialized = False 20 | self.enabled = False 21 | self.soundfonts = [] 22 | def init(self): 23 | self.initialized = True 24 | def inited(self): 25 | return self.initialized 26 | def enabled(self): 27 | return self.enabled 28 | def enable(self): 29 | self.fs = fluidsynth.Synth() 30 | self.enabled = True 31 | def soundfont(self, fn, track, bank, preset): 32 | sfid = self.fs.sfload(fn) 33 | self.fs.program_select(track, sfid, bank, preset) 34 | return sfid 35 | def supported(self): 36 | return not ERROR 37 | def support(self): 38 | return ['fluidsynth','soundfonts'] 39 | def note_on(self, t, n, v): 40 | self.fs.noteon(t, n, v) 41 | def note_off(self, t, n, v): 42 | self.fs.noteoff(t, v) 43 | pass 44 | def stop(self): 45 | pass 46 | 47 | # instrument.export(FluidSynth) 48 | export = FluidSynth 49 | 50 | -------------------------------------------------------------------------------- /textbeat/plugins/sonicpi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import textbeat.instrument as instrument 3 | from textbeat.instrument import Instrument 4 | 5 | ERROR = False 6 | try: 7 | import psonic 8 | except ImportError: 9 | ERROR = True 10 | 11 | class SonicPi(Instrument): 12 | NAME = 'sonicpi' 13 | def __init__(self, args): 14 | Instrument.__init__(self, SonicPi.NAME) 15 | self.initialized = False 16 | def enable(self): 17 | self.initialized = True 18 | def enabled(self): 19 | return self.initialized 20 | def supported(self): 21 | return not ERROR 22 | def support(self): 23 | return ['sonicpi'] 24 | def stop(self): 25 | pass 26 | 27 | # instrument.export(SonicPi) 28 | export = SonicPi 29 | 30 | -------------------------------------------------------------------------------- /textbeat/plugins/supercollider.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import textbeat.instrument as instrument 3 | from textbeat.instrument import Instrument 4 | from shutilwhich import which 5 | 6 | ERROR = False 7 | if which('scsynth'): 8 | try: 9 | import pythonosc 10 | except: 11 | ERROR = True 12 | else: 13 | ERROR = True 14 | 15 | class SuperCollider(Instrument): 16 | NAME = 'supercollider' 17 | def __init__(self, args): 18 | Instrument.__init__(self, SuperCollider.NAME) 19 | self.initialized = False 20 | def enable(self): 21 | self.initialized = True 22 | def enabled(self): 23 | return self.enabled 24 | def supported(self): 25 | return not ERROR 26 | def support(self): 27 | return ['supercollider'] 28 | def stop(self): 29 | pass 30 | 31 | export = SuperCollider 32 | 33 | -------------------------------------------------------------------------------- /textbeat/presets/default.carxp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | false 7 | true 8 | false 9 | 200 10 | 4000 11 | 12 | 13 | 14 | 15 | 16 | 17 | Carla:AudioOut1 18 | AudioOut:playback_1 19 | 20 | 21 | Carla:AudioOut1 22 | AudioOut:playback_1 23 | 24 | 25 | Carla:AudioOut2 26 | AudioOut:playback_2 27 | 28 | 29 | Carla:AudioOut2 30 | AudioOut:playback_2 31 | 32 | 33 | MidiIn:Virtual Raw MIDI 1-0:VirMIDI 1-0 20:0 34 | Carla:MidiIn 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /textbeat/presets/example.carxp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | false 7 | true 8 | false 9 | 200 10 | 4000 11 | 12 | 13 | 14 | 15 | LV2 16 | amsynth 17 | http://code.google.com/p/amsynth/amsynth 18 | 19 | 20 | 21 | Yes 22 | N 23 | 0x0 24 | 25 | 26 | 27 | 28 | 29 | LV2 30 | Helm 31 | http://tytel.org/helm 32 | 33 | 34 | 35 | Yes 36 | N 37 | 0x1 38 | 39 | 40 | 41 | 42 | 43 | Carla:AudioOut1 44 | AudioOut:playback_1 45 | 46 | 47 | Carla:AudioOut1 48 | AudioOut:playback_1 49 | 50 | 51 | Carla:AudioOut2 52 | AudioOut:playback_2 53 | 54 | 55 | Carla:AudioOut2 56 | AudioOut:playback_2 57 | 58 | 59 | MidiIn:Virtual Raw MIDI 1-0:VirMIDI 1-0 20:0 60 | Carla:MidiIn 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /textbeat/presets/festivalrc: -------------------------------------------------------------------------------- 1 | (Parameter.set 'Audio_Required_Format 'aiff) 2 | (Parameter.set 'Audio_Method 'Audio_Command) 3 | (Parameter.set 'Audio_Command "paplay $FILE --client-name=Festival --stream-name=Speech") 4 | -------------------------------------------------------------------------------- /textbeat/presets/test.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | doe 7 | ray 8 | me 9 | fah 10 | sew 11 | lah 12 | tee 13 | doe 14 | 15 | -------------------------------------------------------------------------------- /textbeat/remote.py: -------------------------------------------------------------------------------- 1 | from .defs import * 2 | 3 | 4 | -------------------------------------------------------------------------------- /textbeat/run.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals, print_function, generators 2 | # import textbeat 3 | # def run(): 4 | # textbeat.main() 5 | -------------------------------------------------------------------------------- /textbeat/schedule.py: -------------------------------------------------------------------------------- 1 | from .defs import * 2 | 3 | class Event(object): 4 | def __init__(self, t, func, ch): 5 | self.t = t 6 | self.func = func 7 | self.ch = ch 8 | 9 | class Schedule(object): 10 | def __init__(self, ctx): 11 | self.ctx = ctx 12 | self.events = [] 13 | # store this just in case logic() throws 14 | # we'll need to reenter knowing this value 15 | self.passed = 0.0 16 | self.clock = 0.0 17 | self.last_clock = 0 18 | self.started = False 19 | # all note mute and play events should be marked skippable 20 | def pending(self): 21 | return len(self.events) 22 | def add(self, e): 23 | self.events.append(e) 24 | def clear(self): 25 | assert False 26 | self.events = [] 27 | def clear_channel(self, ch): 28 | assert False 29 | self.events = [ev for ev in self.events if ev.ch!=ch] 30 | def logic(self, t): 31 | processed = 0 32 | self.passed = 0 33 | 34 | # if self.last_clock == 0: 35 | # self.last_clock = time.clock() 36 | # clock = time.clock() 37 | # self.dontsleep = (clock - self.last_clock) 38 | # self.last_clock = clock 39 | 40 | # clock = time.clock() 41 | # if self.started: 42 | # tdelta = (clock - self.passed) 43 | # self.passed += tdelta 44 | # self.clock = clock 45 | # else: 46 | # self.started = True 47 | # self.clock = clock 48 | # self.passed = 0.0 49 | # log(self.clock) 50 | 51 | try: 52 | self.events = sorted(self.events, key=lambda e: e.t) 53 | for ev in self.events: 54 | if ev.t > 1.0: 55 | ev.t -= 1.0 56 | else: 57 | # sleep until next event 58 | if ev.t >= 0.0: 59 | if self.ctx.cansleep and self.ctx.startrow == -1: 60 | self.ctx.t += self.ctx.speed * t * (ev.t-self.passed) 61 | time.sleep(max(0,self.ctx.speed * t * (ev.t-self.passed))) 62 | ev.func(0) 63 | self.passed = ev.t # only inc if positive 64 | else: 65 | ev.func(0) 66 | 67 | processed += 1 68 | 69 | slp = t * (1.0 - self.passed) # remaining time 70 | if slp > 0.0: 71 | self.ctx.t += self.ctx.speed*slp 72 | if self.ctx.cansleep and self.ctx.startrow == -1: 73 | time.sleep(max(0,self.ctx.speed*slp)) 74 | self.passed = 0.0 75 | 76 | self.events = self.events[processed:] 77 | except KeyboardInterrupt: 78 | self.events = self.events[processed:] 79 | raise 80 | except SignalError: 81 | self.events = self.events[processed:] 82 | raise 83 | except EOFError: 84 | self.events = self.events[processed:] 85 | raise 86 | 87 | -------------------------------------------------------------------------------- /textbeat/support.py: -------------------------------------------------------------------------------- 1 | from .defs import * 2 | from shutilwhich import which 3 | import tempfile, shutil 4 | from . import instrument 5 | # from xml.dom import minidom 6 | ARGS = get_args() 7 | SUPPORT = set(['midi']) 8 | SUPPORT_ALL = set(['midi', 'fluidsynth', 'soundfonts']) # gme,mpe,sonicpi,supercollider,csound 9 | MIDI = True 10 | SOUNDFONTS = False # TODO: make this a SupportPlugin ref 11 | AUTO = False 12 | AUTO_MODULE = None 13 | SOUNDFONT_MODULE = None 14 | auto_inited = False 15 | 16 | SUPPORT_PLUGINS = {} 17 | 18 | # load plugins from plugins dir 19 | 20 | import textbeat.plugins as tbp 21 | from textbeat.plugins import * 22 | # search module exports for plugins 23 | plugs = [] 24 | for p in tbp.__dict__: 25 | try: 26 | pattr = getattr(tbp, p) 27 | plugs += [pattr.export(ARGS)] 28 | except: 29 | pass 30 | # plugs = instrument.plugins() 31 | for plug in plugs: 32 | # plug.init() 33 | ps = plug.support() 34 | SUPPORT_ALL = SUPPORT_ALL.union(ps) 35 | if not plug.supported(): 36 | continue 37 | for s in ps: 38 | SUPPORT.add(s) 39 | SUPPORT_PLUGINS[s] = plug 40 | if 'auto' in s: 41 | AUTO = True 42 | AUTO_MODULE = plug 43 | auto_inited = True 44 | if 'soundfonts' in s: 45 | SOUNDFONTS = True 46 | SOUNDFONT_MODULE = plug 47 | 48 | def supports(dev): 49 | global SUPPORT 50 | return dev in SUPPORT 51 | 52 | def supports_soundfonts(): 53 | return SOUNDFONTS 54 | def supports_auto(): 55 | return AUTO 56 | def supports(tech): 57 | return tech in SUPPORT 58 | 59 | def support_stop(): 60 | for plug in plugs: 61 | if plug.inited(): 62 | plug.stop() 63 | 64 | -------------------------------------------------------------------------------- /textbeat/theory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import os, sys 3 | from future.utils import iteritems 4 | from collections import OrderedDict 5 | from .defs import get_defs 6 | from .parser import * 7 | 8 | FLATS=False 9 | SOLFEGE=False 10 | NOTENAMES=True # show note names instead of numbers 11 | 12 | class NoSuchScale(BaseException): 13 | pass 14 | class NoSuchNote(BaseException): 15 | pass 16 | 17 | NOTE_OFFSET_VALUES = [None,1,None,2,None,3,4,None,5,None,6,None,7] 18 | # LETTER_OFFSET_VALUES = [None,'C',None,'D',None,'E','F',None,'G',None,'A',None,'B'] 19 | 20 | SOLFEGE_NOTES ={ 21 | 'do': '1', 22 | 'di': '#1', 23 | 'ra': 'b2', 24 | 're': '2', 25 | 'ri': '#2', 26 | 'me': 'b3', 27 | 'mi': '3', 28 | 'fa': '4', 29 | 'fi': '4#', 30 | 'se': 'b5', 31 | 'sol': '5', 32 | 'si': '#5', 33 | 'le': 'b6', 34 | 'la': '6', 35 | 'li': '#6', 36 | 'te': 'b7', 37 | 'ti': '7', 38 | } 39 | 40 | def note_name(n, nn=NOTENAMES, ff=FLATS, sf=SOLFEGE): 41 | assert type(n) == int 42 | if sf: 43 | if ff: 44 | return ['Do','Ra','Re','Me','Mi','Fa','Se','Sol','Le','La','Te','Ti'][n%12] 45 | else: 46 | return ['Do','Ri','Re','Ri','Mi','Fa','Fi','Sol','Si','La','Li','Ti'][n%12] 47 | elif nn: 48 | if ff: 49 | return ['C','Db','D','Eb','E','F','Gb','G','Ab','A','Bb','B'][n%12] 50 | return ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'][n%12] 51 | else: 52 | if ff: 53 | return ['1','b2','2','b3','3','4','5b','5','b6','6','b7','7'][n%12] 54 | return ['1','#1','2','#2','3','4','#4','5','#5','6','#6','7'][n%12] 55 | 56 | class Scale: 57 | def __init__(self, name, intervals): 58 | self.name = name 59 | self.intervals = intervals 60 | self.modes = [''] * 12 61 | def add_mode(self, name, index): 62 | assert index > 0 63 | self.modes[index-1] = name 64 | def add_mode_i(self, name, index): # chromaticc index 65 | assert index > 0 66 | self.modes[index-1] = name 67 | def mode(self, index): 68 | return self.mode[index] 69 | def mode_name(self, idx): 70 | assert idx != 0 # modes are 1-based 71 | m = self.modes[idx-1] 72 | if not m: 73 | if idx == 1: 74 | return self.name 75 | else: 76 | return self.name + " mode " + str(idx) 77 | return m 78 | 79 | SCALES = {} 80 | MODES = {} 81 | DEFS = get_defs() 82 | for k,v in iteritems(DEFS['scales']): 83 | scale = SCALES[k] = Scale(k, v['intervals']) 84 | i = 1 85 | scaleinfo = DEFS['scales'][k] 86 | if 'modes' in scaleinfo: 87 | for scalename in scaleinfo['modes']: 88 | MODES[scalename] = (k,i) 89 | SCALES[k].add_mode(scalename,i) 90 | i += 1 91 | else: 92 | MODES[k] = (k,1) 93 | 94 | DIATONIC = SCALES['diatonic'] 95 | # for lookup, normalize name first, add root to result 96 | # number chords can't be used with note numbers "C7 but not 17 97 | # in the future it might be good to lint chord names in a test 98 | # so that they dont clash with commands and break previous songs if changed 99 | # This will be replaced for a better parser 100 | # TODO: need optional notes marked 101 | CHORDS = DEFS['chords'] 102 | CHORDS_ALT = DEFS['chord_alts'] 103 | # CHORD_REPLACE = DEFS['chord_replace'] 104 | # replace and keep the rest of the name 105 | CHORDS_REPLACE = OrderedDict([ 106 | ("#5", "+"), 107 | ("aug", "+"), 108 | ("mmaj", "mm"), 109 | ("major", "ma"), 110 | ("M", "ma"), 111 | ("maj", "ma"), 112 | ("minor", "m"), 113 | ("min", "m"), 114 | ("dom", ""), # temp 115 | ("R", ""), 116 | ]) 117 | 118 | # add scales as chords 119 | for sclname, scl in iteritems(SCALES): 120 | # as with chords, don't list root 121 | for m in range(len(scl.modes)): 122 | sclnotes = [] 123 | idx = 0 124 | inter = list(filter(lambda x:x.isdigit(), scl.intervals)) 125 | if m: 126 | inter = list(inter[m:]) + list(inter[:m]) 127 | for x in inter: 128 | sclnotes.append(note_name(idx, False)) 129 | try: 130 | idx += int(x) 131 | except ValueError: 132 | idx += 1 # passing tone is 1 133 | pass 134 | sclnotes = ' '.join(sclnotes[1:]) 135 | if m==0: 136 | CHORDS[sclname] = sclnotes 137 | # log(scl.mode_name(m+1)) 138 | # log(sclnotes) 139 | CHORDS[scl.mode_name(m+1)] = sclnotes 140 | 141 | # certain chords parse incorrectly with note letters 142 | BAD_CHORDS = [] 143 | for chordlist in (CHORDS, CHORDS_ALT): 144 | for k in chordlist.keys(): 145 | if k and k[0].lower() in 'abcdefgiv': 146 | BAD_CHORDS.append(k[1:]) # 'aug' would be 'ug' 147 | 148 | # don't parse until checking next char 149 | # AMBIGUOUS_NAMES = { 150 | # 'io': 'n', # i dim vs ionian 151 | # 'do': 'r', # d dim vs dorian 152 | # } 153 | 154 | def normalize_chord(c): 155 | try: 156 | c = CHORDS_ALT[c] 157 | except KeyError: 158 | c = c.lower() 159 | try: 160 | c = CHORDS_ALT[c] 161 | except KeyError: 162 | pass 163 | return c 164 | 165 | def expand_chord(c): 166 | # if c=='rand': 167 | # log(CHORDS.values()) 168 | # r = random.choice(CHORDS.values()) 169 | # log(r) 170 | # return r 171 | for k,v in iteritems(CHORDS_REPLACE): 172 | cr = c.replace(k,v) 173 | if cr != c: 174 | c=cr 175 | # log(c) 176 | break 177 | 178 | # - is shorthand for m in the index, but only at beginning and end 179 | # ex: -7b5 -> m7b5, but mu-7 -> mum7 is invalid 180 | # remember notes are not part of chord name here (C-7 -> Cm7 works) 181 | if c.startswith('-'): 182 | c = 'm' + c[1:] 183 | if c.endswith('-'): 184 | c = c[:-1] + 'm' 185 | return CHORDS[normalize_chord(c)].split(' ') 186 | 187 | ALIGNED_NOTE_NAMES = [ 188 | ['1','b2','2','b3','3','4','b5','5','b6','6','b7','7'], 189 | ['','#1','','#2','','','#4','','#5','','#6',''], 190 | ['C','Db','D','Eb','E','F','Gb','G','Ab','A','Bb','B'], 191 | ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'], 192 | ['bb2','bbb3','bb3','bb4','b4','bb5','bbb6','bb6','bbb7','bb7',''], 193 | ['','','###1','##2','###2','##3','##4','###4','##5','###5','##6',''], 194 | ['Dbb','Ebbb','Ebb','Fbb','Fb','Gbb','Abbb','Abb','Bbbb','Bbb',''], 195 | ['','','C###','D##','D###','E##','F##','F###','G##','G###','A##',''] 196 | ] 197 | 198 | def note_offset(s): 199 | n = 0 200 | sharps = count_seq(s,'#') 201 | n += sharps 202 | flats = count_seq(s,'b') 203 | n -= flats 204 | s = s[sharps + flats:] 205 | if s: 206 | s = s.lower() 207 | for names in ALIGNED_NOTE_NAMES: 208 | try: 209 | return n + names.index(s) 210 | except ValueError: 211 | pass 212 | raise NoSuchNote() 213 | 214 | -------------------------------------------------------------------------------- /textbeat/track.py: -------------------------------------------------------------------------------- 1 | from .defs import * 2 | import math 3 | 4 | class Recording(object): 5 | def __init__(self, name, slot): 6 | self.name = slot 7 | self.content = [] 8 | 9 | class Tuplet(object): 10 | def __init__(self): 11 | # self.tuplets = False 12 | self.note_spacing = 1.0 13 | self.tuplet_count = 0 14 | self.tuplet_offset = 0.0 15 | 16 | class Lane(object): 17 | def __init__(self, ctx, idx, midich, parent=None): 18 | self.idx = idx 19 | self.midi = ctx.midi 20 | self.ctx = ctx 21 | self.schedule = self.ctx.schedule 22 | def master(self): 23 | return self.parent if self.parent else self 24 | def reset(self): 25 | self.notes = [0] * RANGE 26 | self.sustain_notes = [False] * RANGE 27 | 28 | class Track(Lane): 29 | class Flag: 30 | ROMAN = bit(0) 31 | # TRANSPOSE = bit(1) 32 | FLAGS = [ 33 | 'roman', # STUB: fit roman chord in scale shape 34 | # 'transpose', # allow transposition of note letters 35 | ] 36 | def __init__(self, ctx, idx, midich): 37 | Lane.__init__(self,ctx,idx,midich) 38 | # self.midis = [player] 39 | self.channels = [(0,midich)] 40 | self.midich = midich # tracks primary midi channel 41 | self.initial_channel = midich 42 | self.non_drum_channel = midich 43 | self.reset() 44 | def us(self): 45 | # microseconds 46 | # return int(self.ctx.t)*1000000 47 | return math.floor(mido.second2tick(self.ctx.t, self.ctx.grid, self.ctx.tempo)) 48 | def reset(self): 49 | Lane.reset(self) 50 | self.mode = 0 # 0 is NONE which inherits global mode 51 | self.scale = None 52 | # self.instrument = 0 53 | self.octave = 0 # rel to OCTAVE_BASE 54 | self.modval = 0 # dont read in mod, just track its change by this channel 55 | self.sustain = False # sustain everything? 56 | self.arp_note = None # current arp note 57 | self.arp_notes = [] # list of notes to arpegiate 58 | self.arp_idx = 0 59 | self.arp_notes_left = 0 60 | self.arp_cycle_limit = 0 # cycles remaining, only if limit != 0 61 | self.arp_pattern = [] # relative steps to 62 | self.arp_enabled = False 63 | self.arp_once = False 64 | self.arp_delay = 0.0 65 | self.arp_sustain = False 66 | self.arp_note_spacing = 1.0 67 | # self.arp_reverse = False 68 | self.vel = 100 69 | self.max_vel = -1 70 | self.soft_vel = -1 71 | self.ghost_vel = -1 72 | self.accent_vel = -1 73 | self.non_drum_channel = self.initial_channel 74 | # self.off_vel = 64 75 | self.staccato = False 76 | self.patch_num = 0 77 | self.transpose = 0 78 | self.pitchval = 0.0 79 | self.tuplet = [] # future 80 | self.tuplets = False 81 | self.note_spacing = 1.0 82 | self.tuplet_count = 0 83 | self.tuplet_offset = 0.0 84 | self.use_sustain_pedal = False # whether to use midi sustain instead of track 85 | self.sustain_pedal_state = False # current midi pedal state 86 | # self.schedule.clear_channel(self) 87 | self.flags = 0 # set() 88 | self.enabled = True 89 | self.soloed = False 90 | # self.muted = False 91 | self.volval = 1.0 92 | self.slots = {} # slot -> Recording 93 | self.slot = None # careful, this can be 0 94 | self_slot_idx = 0 95 | self.lane = None 96 | self.lanes = [] 97 | self.ccs = {} 98 | self.dev = 0 99 | 100 | # def _lazychannelfunc(self): 101 | # # get active channel numbers 102 | # return list(map(filter(lambda x: self.channels & x[0], [(1< 0 134 | # if f != f & FLAGS: 135 | # raise ParseError('invalid flags') 136 | self.flags |= f 137 | def has_flags(self, f): 138 | if isinstance(f, str): 139 | f = 1 << FLAGS.index(f) 140 | else: 141 | assert f > 0 142 | # if f != f & FLAGS: 143 | # raise ParseError('invalid flags') 144 | return self.flags & f 145 | def midifile_write(self, ch, msg): 146 | # ch: midi channel index, not midi channel # (index 0 of self.channels tuple item) 147 | while ch >= len(self.ctx.midifile.tracks): 148 | self.ctx.midifile.tracks.append(mido.MidiTrack()) 149 | # print(msg) 150 | self.ctx.midifile.tracks[ch].append(msg) 151 | def enable(self, v=True): 152 | was = v 153 | if not was and v: 154 | self.enabled = v 155 | self.panic() 156 | def disable(self, v=True): 157 | self.enable(not v) 158 | def stop(self): 159 | self.release_all(True) 160 | for ch in self.channels: 161 | status = (MIDI_CC<<4) + ch[1] 162 | if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status,120,0)) 163 | if self.ctx.midifile: 164 | self.midifile_write(ch[0], mido.UnknownMetaMessage(status,data=[120, 0],time=self.us())) 165 | else: 166 | self.midi[ch[0]].write_short(status, 120, 0) 167 | if self.modval>0: 168 | self.refresh() 169 | self.modval = False 170 | def panic(self): 171 | self.release_all(True) 172 | for ch in self.channels: 173 | status = (MIDI_CC<<4) + ch[1] 174 | if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status,123,0)) 175 | if self.ctx.midifile: 176 | self.midifile_write(ch[0], mido.UnknownMetaMessage(status, [123, 0], time=self.us())) 177 | else: 178 | self.midi[ch[0]].write_short(status, 123, 0) 179 | if self.modval>0: 180 | self.refresh() 181 | self.modval = False 182 | def note_on(self, n, v=-1, sustain=False): 183 | if self.use_sustain_pedal: 184 | if sustain and self.sustain != sustain: 185 | self.cc(MIDI_SUSTAIN_PEDAL, sustain) 186 | elif not sustain: # sustain=False is overridden by track sustain 187 | sustain = self.sustain 188 | if v == -1: 189 | v = self.vel 190 | if n < 0 or n > RANGE: 191 | return 192 | for ch in self.channels: 193 | self.notes[n] = v 194 | self.sustain_notes[n] = sustain 195 | # log("on " + str(n)) 196 | if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: NOTE ON (%s, %s, %s)' % (n,v,ch)) 197 | if (not self.ctx.muted or (self.ctx.muted and self.soloed))\ 198 | and self.enabled and self.ctx.startrow==-1: 199 | if self.ctx.midifile: 200 | self.midifile_write(ch[0], mido.Message( 201 | 'note_on',note=n,velocity=v,time=self.us(),channel=ch[1] 202 | )) 203 | else: 204 | self.midi[ch[0]].note_on(n,v,ch[1]) 205 | def note_off(self, n, v=-1): 206 | if v == -1: 207 | v = self.vel 208 | if n < 0 or n >= RANGE: 209 | return 210 | if self.notes[n]: 211 | # log("off " + str(n)) 212 | for ch in self.channels: 213 | if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: NOTE OFF (%s, %s, %s)' % (n,v,ch)) 214 | if not self.ctx.midifile: 215 | self.midi[ch[0]].note_on(self.notes[n],0,ch[1]) 216 | self.midi[ch[0]].note_off(self.notes[n],v,ch[1]) 217 | self.notes[n] = 0 218 | self.sustain_notes[n] = 0 219 | if self.ctx.midifile: 220 | self.midifile_write(ch[0], mido.Message( 221 | 'note_on',note=n,velocity=0,time=self.us(),channel=ch[1] 222 | )) 223 | self.midifile_write(ch[0], mido.Message( 224 | 'note_off',note=n,velocity=v,time=self.us(),channel=ch[1] 225 | )) 226 | 227 | self.cc(MIDI_SUSTAIN_PEDAL, True) 228 | def release_all(self, mute_sus=False, v=-1): 229 | if v == -1: 230 | v = self.vel 231 | for n in range(RANGE): 232 | # if mute_sus, mute sustained notes too, otherwise ignore 233 | mutesus_cond = True 234 | if not mute_sus: 235 | mutesus_cond = not self.sustain_notes[n] 236 | if self.notes[n] and mutesus_cond: 237 | for ch in self.channels: 238 | if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: NOTE OFF (%s, %s, %s)' % (n,v,ch)) 239 | self.midi[ch[0]].note_on(n,0,ch[1]) 240 | self.midi[ch[0]].note_off(n,v,ch[1]) 241 | self.notes[n] = 0 242 | self.sustain_notes[n] = 0 243 | # log("off " + str(n)) 244 | # self.notes = [0] * RANGE 245 | if self.modval>0: 246 | self.cc(1,0) 247 | # self.arp_enabled = False 248 | # self.schedule.clear_channel(self) 249 | # def cut(self): 250 | def midi_channel(self, midich, stackidx=-1): 251 | if midich==DRUM_CHANNEL: # setting to drums 252 | if self.channels[stackidx][1] != DRUM_CHANNEL: 253 | self.non_drum_channel = self.channels[stackidx][1] 254 | self.octave = DRUM_OCTAVE 255 | else: 256 | for ch in self.channels: 257 | if ch!=DRUM_CHANNEL: 258 | midich = ch[1] 259 | if midich != DRUMCHANNEL: # no suitable channel in span? 260 | midich = self.non_drum_channel 261 | if stackidx == -1: # all 262 | self.release_all() 263 | self.channels = [(0,midich)] 264 | elif midich not in self.channels: 265 | self.channels.append(midich) 266 | def pitch(self, val): # [-1.0,1.0] 267 | val = min(max(0,int((1.0 + val)*0x2000)),16384) 268 | self.pitchval = val 269 | val2 = (val>>0x7f) 270 | val = val&0x7f 271 | for ch in self.channels: 272 | status = (MIDI_PITCH<<4) + ch[1] 273 | if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: PITCH (%s, %s)' % (val,val2)) 274 | if self.ctx.midifile: 275 | self.midifile_write(ch[0],mido.UnknownMetaMessage(status,data=[val1,val2], time=self.us())) 276 | else: 277 | self.midi[ch[0]].write_short(status,val,val2) 278 | def cc(self, cc, val): # control change 279 | if type(val) ==type(bool): val = 127 if val else 0 # allow cc bool switches 280 | for ch in self.channels: 281 | status = (MIDI_CC<<4) + ch[1] 282 | if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status, cc,val)) 283 | if self.ctx.midifile: 284 | self.midifile_write(ch[0], mido.UnknownMetaMessage(status,data=[cc,val],time=self.us())) 285 | else: 286 | self.midi[ch[0]].write_short(status,cc,val) 287 | self.ccs[cc] = v 288 | if cc==1: 289 | self.modval = val 290 | if cc==7: 291 | self.volval = val/127.0 292 | def mod(self, val): 293 | self.modval = 0 294 | return self.cc(1,val) 295 | def patch(self, p, stackidx=-1): 296 | if isinstance(p,basestring): 297 | # look up instrument string in GM 298 | i = 0 299 | inst = p.replace('_',' ').replace('.',' ').lower() 300 | 301 | if p in DRUM_WORDS: 302 | self.midi_channel(DRUM_CHANNEL) 303 | p = 0 304 | else: 305 | if self.midich == DRUM_CHANNEL: 306 | self.midi_channel(self.non_drum_channel) 307 | 308 | stop_search = False 309 | gmwords = GM_LOWER 310 | for w in inst.split(' '): 311 | gmwords = list(filter(lambda x: w in x, gmwords)) 312 | lengw = len(gmwords) 313 | if lengw==1: 314 | break 315 | elif lengw==0: 316 | log(FG.RED + 'Patch \"'+p+'\" not found') 317 | assert False 318 | assert len(gmwords) > 0 319 | if self.ctx.shell: 320 | log(FG.GREEN + 'GM Patch: ' + STYLE.RESET_ALL + gmwords[0]) 321 | p = GM_LOWER.index(gmwords[0]) 322 | # for i in range(len(GM_LOWER)): 323 | # continue_search = False 324 | # for pword in inst.split(' '): 325 | # if pword.lower() not in gmwords: 326 | # continue_search = True 327 | # break 328 | # p = i 329 | # stop_search=True 330 | 331 | # if stop_search: 332 | # break 333 | # if continue_search: 334 | # assert i < len(GM_LOWER)-1 335 | # continue 336 | 337 | self.patch_num = p 338 | # log('PATCH SET - ' + str(p)) 339 | if stackidx==-1: 340 | for ch in self.channels: 341 | status = (MIDI_PROGRAM<<4) + ch[1] 342 | if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: PROGRAM (%s, %s)' % (status,p)) 343 | self.midi[ch[0]].write_short(status,p) 344 | else: 345 | status = (MIDI_PROGRAM<<4) + self.channels[stackidx][1] 346 | if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: PROGRAM (%s, %s)' % (status,p)) 347 | self.midi[self.channels[stackidx][0]].write_short(status,p) 348 | 349 | def bank(self, b): 350 | self.ccs[0] = b 351 | self.cc(0,b) 352 | def arp(self, notes, count=0, sustain=False, pattern=[], reverse=False, octave=0): 353 | self.arp_enabled = True 354 | if reverse: 355 | notes = notes[::-1] 356 | self.arp_notes = list(map(lambda n: n + (octave*12), notes)) 357 | self.arp_cycle_limit = count 358 | self.arp_cycle = count 359 | self.arp_pattern = pattern if pattern else [1] 360 | self.arp_pattern_idx = 0 361 | self.arp_notes_left = len(notes) * max(1,count) 362 | self.arp_idx = 0 # use inversions to move this start point (?) 363 | self.arp_once = False 364 | self.arp_sustain = sustain 365 | def arp_stop(self): 366 | self.arp_enabled = False 367 | self.release_all() 368 | def arp_next(self, stop_infinite=True): 369 | stop = False 370 | assert self.arp_enabled 371 | # if not self.arp_enabled: 372 | # self.arp_note = None 373 | # return False 374 | # out(self.arp_idx + 1) 375 | if self.arp_notes_left != -1 or stop_infinite: 376 | if self.arp_notes_left != -1: 377 | self.arp_notes_left = max(0, self.arp_notes_left - 1) 378 | if self.arp_notes_left <= 0: 379 | if self.arp_cycle_limit or stop_infinite: 380 | self.arp_note = None 381 | self.arp_enabled = False 382 | self.arp_note = self.arp_notes[self.arp_idx] 383 | self.arp_idx = self.arp_idx + self.arp_pattern[self.arp_pattern_idx] 384 | self.arp_pattern_idx = (self.arp_pattern_idx + 1) % len(self.arp_pattern) 385 | self.arp_delay = (self.arp_delay + self.arp_note_spacing) - 1.0 386 | if self.arp_idx >= len(self.arp_notes) or self.arp_idx < 0: # cycle? 387 | self.arp_once = True 388 | if self.arp_cycle_limit: 389 | self.arp_cycle -= 1 390 | if self.arp_cycle == 0: 391 | self.arp_enabled = False 392 | self.arp_idx = 0 393 | # else: 394 | # self.arp_idx += 1 395 | return self.arp_note != None 396 | def arp_restart(self, count = None): 397 | self.arp_enabled = True 398 | # self.arp_sustain = False 399 | if count != None: # leave same (could be -1, so use -2) 400 | self.arp_count = count 401 | self.arp_idx = 0 402 | def tuplet_next(self): 403 | delay = 0.0 404 | if self.tuplets: 405 | delay = self.tuplet_offset 406 | self.tuplet_offset += self.note_spacing - 1.0 407 | # if self.tuplet_offset >= 1.0 - EPSILON: 408 | # out('!!!') 409 | # self.tuplet_offset = 0.0 410 | self.tuplet_count -= 1 411 | if not self.tuplet_count: 412 | self.tuplet_stop() 413 | # else: 414 | # self.tuplet_stop() 415 | # if feq(delay,1.0): 416 | # return 0.0 417 | # out(delay) 418 | return delay 419 | def tuplet_stop(self): 420 | self.tuplets = False 421 | self.tuplet_count = 0 422 | self.note_spacing = 1.0 423 | self.tuplet_offset = 0.0 424 | 425 | -------------------------------------------------------------------------------- /textbeat/tutorial.py: -------------------------------------------------------------------------------- 1 | from .defs import * 2 | 3 | class Tutorial(object): 4 | def __init__(self, player): 5 | self.player = player 6 | player.shell = True 7 | self.idx = 0 8 | def next(self): 9 | pass 10 | # print(MSG[self.idx]) 11 | # self.idx += 1 12 | 13 | -------------------------------------------------------------------------------- /textbeat/tutorial.yaml: -------------------------------------------------------------------------------- 1 | tutorial: 2 | - Introduction: 3 | - text: | 4 | Welcome to the textbeat tutorial! 5 | Make sure your sound is on. 6 | First, let's make sure you can hear midi. 7 | prompt: | 8 | Type the the number 1 and hit enter. 9 | answer: 10 | - '1' 11 | - text: | 12 | Did you hear a note? It should probably sound like a low quality piano. 13 | If you didn't, you need to check the manual for 14 | setup instructions. Textbeat does not (yet) come with builtin sounds. 15 | You have to have something else that will play the notes! 16 | prompt: 17 | Everything good? Type 1 to continue. 18 | answer: 19 | - '1' 20 | - text: | 21 | Alright, textbeat prefers note numbers. But if you really like letters, 22 | you can use those too. By default, 1 is C. Try it! 23 | prompt: 24 | Type C to play the note. 25 | answer: 26 | - C 27 | - text: | 28 | You are typing into the textbeat(txbt) shell. 29 | Usually, you'd write songs in textbeat files (.txbt), 30 | but this is a good enough place to start and learn what 31 | commands do what. 32 | 33 | There is one important difference between the shell and .txbt files. 34 | 35 | In the shell, you can type notes and they'll be played in sequence, 36 | one after another. 37 | But in .txbt files, everything is written vertically in columns, 38 | so we can have different instruments on the same row. 39 | 40 | Let's play some notes in sequence. Type the notes below. 41 | prompt: 42 | '1 2 3' 43 | - text: | 44 | Woah, slow down there, Mozart. It's time to change the tempo. 45 | Currently we're at 120bpm (beats per minute) with 4 grid subdivisions. 46 | 47 | The subdivisions make the note faster, so let's slow that down. 48 | 49 | Use the t and x global (%) commands to slow things down. 50 | prompt: 51 | '%t40x1' 52 | - text: | 53 | Alright, we're now at 40 beats per minute with no subdivisions From the top again! 54 | prompt: 55 | '1 2 3' 56 | - text: | 57 | Okay, let's try playing a scale. This is called the major scale. It goes 58 | from 1 all the way up to 1 in a higher octave. We mark the higher octave 59 | using \' 60 | prompt: 61 | "1 2 3 4 5 6 7 1'" 62 | - text: | 63 | We can use the higher octave with apostrophe (\') and lower with comma (,) 64 | prompt: 65 | "1, 1 1'" 66 | - text: | 67 | Let's stack 3 octaves of the same note. 68 | 69 | The slash implies moving down to the next octave. 70 | 71 | Musicians will hate me right now if I don't mention this. This 72 | syntax definition is SPECIFIC to textbeat. There is something similar in music 73 | called a slash chord (C/E). 74 | 75 | In textbeat, the slash just means to stack the notes or chords. 76 | prompt: 77 | "1/1/1" 78 | - text: | 79 | Let's suffix this with a '&' to hear the notes played in sequence. 80 | 81 | In music, we call this an arpeggio. We use the & symbol because it kind of 82 | looks like an A for arpeggio. 83 | prompt: 84 | "1/1/1&" 85 | - text: | 86 | Alright, I'm bored of octaves, let's play a chord. 87 | prompt: 88 | 'sus' 89 | - text: | 90 | You just played a sus chord (also called sus4). Sus stands for suspended. 91 | 92 | It contains the notes 1, 4, and 5 (relative to where we position it). 93 | 94 | Like octaves, sus has a very pure sound. Let's arpeggiate it 3 times. 95 | 96 | You\'ve probably heard this chord before in piano runs. 97 | prompt: 98 | 'sus&3' 99 | - text: | 100 | Chords can be positioned wherever. If we don't write a number, that 101 | just means its on 1, or the note C. 102 | 103 | We can move the whole chord by writing a note number or letter before it. 104 | 105 | Let's position it different places. 106 | prompt: 107 | '1sus 2sus 3sus' 108 | - text: | 109 | Let's introduce strum ($) chords. In textbeat speak, this just opens 110 | them up and plays them all in a single grid space, 111 | instead of walking them slowly like the arpeggio (&) command. 112 | 113 | We'll revisit strumming later, but until then, here it is. 114 | prompt: 115 | '1sus$ 2sus$ 3sus$' 116 | - text: | 117 | Alright, next, the major chord. Let's arpeggiate (&) it. 118 | 119 | Some people say major chords sound happy or accomplished. 120 | 121 | There are a few ways to write this one (ma, maj, major, etc.) 122 | prompt: 123 | 'ma&' 124 | - text: | 125 | Feeling happy yet? Maybe this one will fit your mood more. 126 | 127 | Here's a minor chord. 128 | prompt: 129 | 'm&' 130 | - text: | 131 | Minor sounds darker than major, maybe even sad. 132 | 133 | Alright let's walk some chords 134 | 135 | Some major and some minor. Pay attention! 136 | 137 | I also slipped in a cool new one. 138 | prompt: 139 | '1ma 2m 3m 4ma 5ma 6m 7o 1ma' 140 | - text: | 141 | So we're moving the chords up while changing the chord shapes 142 | as we go to what sounds the best. 143 | 144 | It might be worth mentioning, you can do the same thing with roman numerals. 145 | 146 | The number implies position like the prefixes above, but they are 147 | not notes by default, but major and minor chords depending on the case. 148 | 149 | The 'o' is short for diminished chord, and in our major scale you'll 150 | find it on note 6. 151 | 152 | Let's play these chords! 153 | prompt: 154 | "I ii iii IV V vi viio I'" 155 | - text: | 156 | Alright, so you know that diminished chord of 7? 157 | It probably sounded good in this context, but if you play it by 158 | itself it sounds a little bit unresolved. Check it out. 159 | prompt: 160 | 'o&' 161 | - text: | 162 | Sus, major, minor, and diminished. That's a lot to take in. 163 | 164 | Let's end with one more command. The underscore (_) sustains the notes 165 | like a piano pedal. 166 | 167 | Let's combine the strum and hold sustain commands ($_) and do 168 | a nice piano run! 169 | prompt: 170 | 'ma/ma/1$_' 171 | - text: | 172 | It's probably time for you to go experiment with what you've learned. 173 | 174 | Come back for part II by typing tutorial2 in the shell. 175 | 176 | -------------------------------------------------------------------------------- /txbt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PYTHONPATH=`dirname $0` python -m textbeat $* 3 | -------------------------------------------------------------------------------- /txbt.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | set PYTHONPATH=%~dp0 3 | py -3 -m textbeat %* --------------------------------------------------------------------------------