diff --git a/.gitignore b/.gitignore index 0d20b64..200c438 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,10 @@ +__pycache__/ +build/ +dist/ +textbeat.egg-info/ +examples/*.mid +build.sh +clean.sh +*.old *.pyc +lint.txt diff --git a/.pylintrc b/.pylintrc index ff10e03..d15f01f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,2 +1,2 @@ [MESSAGES CONTROL] -disable=C0326,C0303 +disable=C0326,C0303,R1714 diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..bf6a767 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-include textbeat/def * +recursive-include textbeat/presets * diff --git a/README.md b/README.md index 2a2da7a..6e0d2df 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# ![Decadence](icon.png) decadence +# textbeat -Plaintext music tracker and midi shell. +Plaintext music sequencer and interactive shell. Write music in vim or your favorite text editor. @@ -10,9 +10,11 @@ Open-source under MIT License (see LICENSE file for information) Copyright (c) 2018 Grady O'Connell -- [Gitter Chat](https://gitter.im/flipcoder/decadence) -- [Project Board](https://trello.com/b/S8AsJaaA/decadence) -- Vim integration: [vim-decadence](https://github.com/flipcoder/vim-decadence) +- [Project Board](https://trello.com/b/S8AsJaaA/textbeat) +- Vim integration: [vim-textbeat](https://github.com/flipcoder/vim-textbeat) + +**This project is still very new. Despite number of features, you may quickly +run into issues, especially with editor integration.** # Overview @@ -20,18 +22,15 @@ Compose music in a plaintext format or type music directly in the shell. The format is vertical and column-based, similar to early music trackers, but with syntax inspired by jazz/music theory. -**This project is still very new. Despite number of features, you may quickly -run into issues. Feel free to [communicate](https://gitter.im/flipcoder/decadence) your experiences to me.** - # Features -Decadence is a new project, but you can already do lots of cool things: +Textbeat is still in development, but you can already do lots of cool things: - Strumming - Arpeggiation - Tuplets and polyrhythms -- CC automation -- Vibrato, pitch, and mod wheels +- MIDI CC automation +- Vibrato, pitch, and mod wheel control - Dynamics - Accents - Velocity @@ -40,61 +39,62 @@ Decadence is a new project, but you can already do lots of cool things: - Note length - Delays - Scales and modes by name -- Markers, repeats (WIP) +- Markers, repeats, callstack # Setup -You can use the shell with General Midi out-of-the-box on windows, which is great for learning, -but sounds bad without a decent soundfont. +## Linux -If you want to use VST instruments, you'll need to route the MIDI out to something that hosts them, like a DAW. +``` +git clone https://github.com/flipcoder/textbeat +cd textbeat +sudo python setup.py install +textbeat +``` -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. +## Windows -If you're on Linux, you can use soundfonts through qsynth or use a software instrument like helm or dexed. VSTs should work here as well. +``` +git clone https://github.com/flipcoder/textbeat +cd textbeat +pip3 install -r requirements.txt +./txbt.cmd +``` -If you feed the MIDI into a DAW you'll be able to record the output through the DAW itself. -I'm currently looking into recording via a headless host. +## Test it out! -# Notes and Chords +Once you're in textbeat, try this: -In a traditional tracker, individual notes would take place over multiple -channels. You can do this in decadence if it fits your writing style, -but it is not the only way. +``` +maj& +``` -In a tracker, a C major chord would be specified in a way a -computer would understand it: 3 notes across 3 separate channels: C,E,G (or 1,3,5). -Writing this in a tracker with 1 note per channel is specific enough for a computer, -but annoyingly lacking context from the perspective of a performer. +If you don't hear 3 notes, you need to set up midi (this is the case with Linux). -In decadence, you can write the chord directly: 1maj or Cmaj. -1 ('C') is not required here. as chords without note names are positioned on 1 ('C') (ex. "maj" = "Cmaj" = "1maj"). -Other shorthand names that work: "ma", "major", "M", or roman numeral "I" +## How to set up midi -``` -# note: some of these features are not finished +You can use the shell with General Midi out-of-the-box on windows, which is great for learning, +but sounds bad without a decent soundfont. -- < or >: inversion suffix - - ex: maj> means maj 1st inversion - - repeatable (ex. maj>> means 2nd inversion: 5 1' 3' or G C' E') - - or specify a number (like maj>2), meaning 2nd inversion (this will be useful for scale modes later) -- /: slash: layer chords across octaves (note: different from music theory interpretation) - - repeat slash for multiple octaves (ex. maj//1) -- add (suffix), add note to chord (ex. maj7add11) -- no (suffix): remove a note by number -- |: stack: combines chords/notes manually (ex. maj|sus|#11) -``` +If you want to use VST instruments, you'll need to route the MIDI out to something that hosts them, like a DAW. +(I'm currently working on headless VST rack generation.) -There are a few quirks with the parser that make the chord interpretation different than -what musicians would expect. For example, slash chords do not imply inversions, -but are for stacking across octaves. Additionally, note names alone do no imply chords. -For example, C/E means play a C note with an E in a lower octave, whereas a musician might -interpret this as a specific chord voicing. (Inversions use shift operator (maj> for first inversion)) +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. + +If you're on Linux, you can use soundfonts through qsynth or use a software instrument like helm or dexed. I recommend qsynth. -# The Basics +VSTs should work here as well but you need to pick a host. + +If you feed the MIDI into a DAW you'll be able to record the output through the DAW itself. + +I'm currently looking into export options and recording via a headless host. + +# Tutorial If you're familiar with trackers, you may pick this up quite easily. -Music flows vertically, with separate columns that are separated by whitespace or + +First start by creating a .txbt (textbeat) file, inside the file music +flows vertically, with separate columns that are separated by whitespace or manually setting a column width. Each column represents a track, which defaults to separate midi channel numbers. @@ -129,12 +129,47 @@ Musicians can think of grid as fractions of quarter note, The grid is the beat/quarter-note subdivision. Both Tempo and Grid can be decimal numbers as well. +For example, if you made some chords and you only want +one chord to be played per bar (eg 4 beats) +you could set `%t120x0.25`. -## Transposition and Octaves +You can listen to what you've made by running: -Notice the bottom line has an extra apostrophe character ('). This plays the note in the next octave +``` +textbeat +``` + +Consult the output of `textbeat -h` for further information. + +## Note Numbers + +Both note numbers and letters are supported. +This tutorial will use 1,2,3,4,5,6,7 instead of C,D,E,F,G,A,B. +I'm a fan of thinking about notes without implying a key. +For this reason, textbeat prefers the relative/transposed note numbers +over arbitrary note names. +If you're writing a song in D minor, you may choose to set the global or track key +to D, making D note 1. (You could also set D to 6 if you're thinking modally) +If this is confusing or not beneficial to you: don't worry, it's optional! + +In this format, flats and sharps are prefixed instead of suffixed (b7 ("flat 7") instead of Bb ("B flat")). + +Be aware that this flexibility introduces a few limits with chord names: +- B7 chords should not be written as 'b7', because this means flat 7 +- 7 is a note when used alone, not a chord: + - Write it as dom7 + - Alternatively write 1:7, R7, or C7 +- 27 is not a 7 chord on 2, it's note 27 + - Write it as 2:7 or 2dom7 + +## Transposing Octaves + +In the first example, the apostrophe character (') was used to play the note in the next octave. For an octave below, use a comma (,). -You can use a number value instead to make the octave changes persistent (,2). + +Repeat these for additional octaves (,,, for 3 down, '' for 2 up, etc). + +To make octave changes persist, use a number for the octave count instead of repeating (,2). ## Holding Muting @@ -142,10 +177,10 @@ Notes will continue playing automatically, until they're muted or another note i You can mute all notes in a track with - -To control releasing of notes, use dash (-). The period (.) is simply a placeholder, so notes continue to be played through them. +To control releasing of notes, use dash (-). ``` -; hold note 1 until next note (dots aren't notes, just empty placeholders for example) +; hold note 1 until next note 1 @@ -167,7 +202,7 @@ To control releasing of notes, use dash (-). The period (.) is simply a placeho Note durations can be manually controlled by adding * to increase value by powers of two, You can also add a fractional value to multiply this. These types of fraction -values are used throughout decadence. +values are used throughout textbeat. The opposite of this is the dot (.) which halves note values @@ -195,7 +230,7 @@ Now with dots for staccato: ``` Notes that are played in the same track as other notes mute the previous notes. -In order to overide this, hold a note by suffixing it with underscore (_). +In order to override this, hold a note by suffixing it with underscore (_). A (-) character will then mute them all. @@ -211,9 +246,12 @@ A (-) character will then mute them all. If you want to hold a series of notes like a sustain pedal, simply use two underscores (__) and all future notes will be held until a mute is received. -## Chord +## Chords -You can play notes individually or use chord names. +Unlike traditional trackers, you can write chords directly: 1maj or Cmaj. +1 ('C') is not required here. as chords without note names are positioned on 1 +('C') (ex. "maj" = "Cmaj" = "1maj"). +Other shorthand names that work: "ma", "major", "M", or roman numeral "I" Let's play a scale with some chords: @@ -229,10 +267,12 @@ Let's play a scale with some chords: 1maj' ``` -There are lots of chords and voicings (check config/def.yaml) and I'll be adding a lot more. +There are lots of chords and voicings (check def/ files) and I'll be adding a lot more. All scales and modes are usable as chords, so arpeggiation and strumming is usable with those as well. -# Arpeggios and Strumming +Remember: The note goes *before* the chord, so 7maj is a maj chord on note 7 (i.e. Bmaj), NOT a maj7. + +## Arpeggios and Strumming Chords can be walked if they are suffixed by '&' Be sure to rest in your song long enough to hear it cycle. @@ -263,7 +303,7 @@ To strum, use the hold (_) symbol with this. maj$_ ``` -# Accents +## Velocity and Accents Use a ! or ? to accent or soften a note respectively. @@ -291,7 +331,7 @@ Use values after accent to set a specific velocity: 5!05 # 5% ``` -# Note grouping +## Note grouping For readability, notes can be indented to imply downbeat or grouping @@ -306,37 +346,42 @@ For readability, notes can be indented to imply downbeat or grouping 4 ``` -# Velocity and Gain/Volume +## Volume -Control velocity and volume of notes using the %v## or !## flags respectfully -Example: %v0 in min, %v9 is 90%. But also: %v00 is minimum, %v99 is 99% (%v by itself is full) +Usually you'll want to control velocity through accenting('!') or softening('?') +or using values (!30 for 30%) -Interpolation not yet impl - +If you wish to control volume/gain directly, use @v + ``` -1maj%v9 +1maj@v9 - -1maj%v6 +1maj@v6 - -1maj%v4 +1maj@v4 - -1maj%v2 +1maj@v2 - ``` -# Articulation +Unlike accents, volume changes persist. + +Interpolation is not yet implemented + +## Vibrato, Pitch, and Mod Wheel + +To add vibrato to a note, suffix it with a tilda (~). -Tilda(~) does vibrato. Vibrato uses the mod wheel right now, but will eventually use pitch wheel oscillation. In the future, articulation will be programmable, per-track or per-song. -# Arpeggio Modulation +## Arpeggio Modulation Notes of arpeggios can be modified as they're running, -by having effects in a grid space, for example: +by having effects in the grid space they occur, for example: -` +``` maj7& .? .? @@ -345,17 +390,19 @@ maj7& .? .? .? -` +``` maj7& starts a repeating 4-note arpeggio, and we indent to show this. -Certain notes of the sequence are modulated with short/staccato '.', soft '?' and accent '!' +Certain notes of the sequence are modulated with short/staccato '.', soft '?', and accent '!' For staccato usage w/o a note name, an extra dot is required since '.' is simply a placeholder. -# Tracks +## Tracks + +Columns are separate tracks, line them up for more than one instrument. -Columns are separate tracks, line them up for more than one instrument +The dots are placeholders. ``` 1,2 1 @@ -367,9 +414,9 @@ Columns are separate tracks, line them up for more than one instrument . 1'' ``` -Columns can be detected in some cases, but you'll probably want to -specify the column width manually at the top -(which allows vim to mark the columns), +Columns can be detected (in some cases), but you'll probably want to +specify the column width manually at the top, +which allows vim to mark the columns. ``` # sets column width to 8 @@ -383,145 +430,301 @@ For best view in an editor, it is recommended that you offset the first column b %c=8,-2 ``` -# Patches +## Patches Another useful global var is 'p', which sets midi patches by name or number -across the tracks. The midi names support partial matches (case-insensitive). +across the tracks. The midi names support both patch numbers and partial case-insensitive +matches of GM instruments. ``` %t120 x2 p=piano,guitar,bass,drums c8,-2 ``` -For a full list of GM names, see [config/gm.yaml](https://github.com/flipcoder/decadence/blob/master/config/gm.yaml). +For a full list of GM names, see [def/gm.yaml](https://github.com/flipcoder/textbeat/blob/master/textbeat/def/gm.yaml). -# Markers +## Tuplets -Still working on this feature, it might be broken +The 'T' (tuplet) gives us access to the musical concept of tuplets (called triplets in cases of 3). +which allows note timing and durations to fall along a ratio instead of the usual note subdivisions. -':' sets marker and '@' loops to it. - -``` -:markername -@makername -``` - -Repeat counting, callstack, etc. coming shortly. Code almost done. - -# Tuplets - -Very early support for this. See tuplet.dc example. -The 't' command spreads a set of notes across a tuplet grid, -starting at the first occurence of t in that group. +Tuplets are marked by 'T' and have an optional value at the first occurrence in that group. Ratios provided will control expansion. Default is 3:4. If no denominator is given, it will default to the next power of two (so 3:4, 5:8, 7:8, 11:16). -So in other words if you need a 5:6, you'll need to write t5:6. :) -The ratio of the beat saves. You only need to specify it once per group. -For nested tripets, group those by adding an extra 't'. +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. +The ratio of the tuplet persists for the rest of the grouping. +For nested tripets, group those by adding an extra 'T'. -Consider the 2 tracks: +The two tracks below are a basic usage of triplets: ``` -1 1t -2 2t -3 3t +1 1T +2 2T +3 3T 4 -1 1t -2 2t -3 3t +1 1T +2 2T +3 3T 4 ``` -The spacing is not even between the sets, but the 't' value stretches them -to make them line up in a default ratio of 3:4. +The first column is playing notes along the grid normally, while the +2nd column is playing 3 notes in the space of the others' 4 notes. -# Picking +Even though there is visual spacing between the triplet groups, the 'T' value effective +stretches the notes so they occur along a slower grid according to that ratio. -[Currently designing this feature](https://trello.com/c/D01rlTWp/26-picking) +The spaces that occur after (and between) tuplet groupings should remain empty, +since they are spacers to make the expansion line up. -# Key changes +## Picking -The following behavior is optional and probably not useful to many musicians. +[Currently designing this feature](https://trello.com/c/D01rlTWp/26-picking) -However, beginners may find inspiration by picking a scale instead of using -sharps and flats. +## Key changes -The current scale is currently accessible as a global variable (this will be per-track soon). +``` +# change key (this will change the key of the current scale to 3 (E)) +%k=3 -To set the notes to match scale, +# to set a relative key, this will go from a major scale to relative minor scale +%k+6 -``` -# set notes to dorian mode (won't change key) -%s=dorian +# you can also go downwards +%k-6 -# rotate/relative scale (will move the key note) -%r=dorian +# scale names are supported, this changes the scale shape to dorian +%s=dorian -# in either case, you can also use mode numbers +# you can also use mode numbers %s=2 -%r=2 ``` -# And here's what it looks like +## Chords (Advanced) -``` -%t=120x4 p=piano,bass,drums c=20,-2 +In textbeat, slash (/) chords do not imply inversions, +but are for spanning chord voicings across octaves. Additionally, note names alone do no imply chords. +For example, C/E means play a C note with an E in a lower octave, whereas a musician might +interpret this as a specific chord voicing. Inversions in textbeat uses shift operator (>) instead (maj> for maj first inversion)) -M. 3 1 - m?. - m?. -M. - m?. 3 -M. - m?. - m?. -M. 1 - m?. - m?. -M. b7 - m?. 3 - m?. -M. - m?. -M. 1 - m?. - m?. -M. - m?. 3 - m?. 5 +``` +b7maj7#4/sus2/1 +# same thing with note names: Bbmaj7#4/Csus2/C +# suffix this with & to hear the notes walked individually ``` +The above chord voicing spans 3 octaves and contains 9 notes. +It is a Bbmaj7 chord w/ an added #4 (relative to Bb, which is E), followed by a lower octave Csus2. +Then at the bottom, there is a C bass note. -# Full list of scales, modes, chords, and voicings +## Examples -- Default: [def/default.yaml](https://github.com/flipcoder/decadence/blob/master/def/default.yaml). -- Informal: [def/informal.yaml](https://github.com/flipcoder/decadence/blob/master/def/informal.yaml). -- Experimental: [def/dc.yaml](https://github.com/flipcoder/decadence/blob/master/def/dc.yaml). +Check out the examples/ folder. Play them with textbeat from the +command line: -These lists does not include certain chord modifications (add, no, drop, etc.) +``` +./txbt examples/jazz.txbt +``` # Advanced +## Markers / Repeats + +Here are the marker/repeat commands: + +``` +- |: set marker +- |name: set marker 'name' +- :| goes back to last marker, or start +- :name| goes back to last marker 'name' +- :N| goes back to last marker N number of times +- :name*N| goes back to last marker 'name' N number of times +- || return/pop to last position after marker jump +- ||| end the song here +``` + +## Command line parameters (use -): + +``` +- (default) starts midi shell +- (filename): plays file +- c: play a given sequence + - Passing "1 2 3 4 5" would play those note one after another +- l: play a single line from the file + - Not too useful yet, since it doesn't parse context +- +: play range, comma-separated (+start,end) + - Line numbers and marker names work +- t: tempo +- x: grid +- n: note value +- c: columns + - specify width and optional shift, instead of using auto-detect + - positive shift values create a "gutter" to the left + - negative values eat into the size of the first column +- p: set midi patches + - command-separated list of patches across tracks + - GM instruments names fuzzy match (Example: Piano,Organ,Flute) +- --sharps: Prefer sharps +- --solfege: Use solfege in output (input not yet supported) +- --flats: Prefer flats (currently default) +- --device=DEVICE: Set midi-device (partial match supported) +``` + +## Global commands: + +``` +- %: set var (ex. %P=piano T=120x2 S=dorian) + - K: set key/transpose + - Both absolute and relative values supported + - Relative values are 1-indexed using numbered note name + - whole step: %k+2 whole step + - half step: %k+#1 or %k+b2 + - invalid example (because of 1-index): %k+1 + - O: set global octave + - R: set scale (relative) + - Names and numbers supported + - S: set scale (parallel) + - Names and numbers supported + - P: set patch(s) across channels (comma-separated) + - Matches GM midi names + - Supports midi patch numbers + - General MIDI name matching +- ;: comment +- ;;: cell comment (not yet impl) + +To do relative values, drop the equals sign: +%k-2 +``` + +## Track commands + +``` +- ': play in octave above + - repeat for each additional octave (''') + - for octave shift to persist, use a number instead of repeats ('3) +- ,: play in octave below + - number provided for octave count, default 1 (,,,) + - for octave shift to persist, use a number instead of repeats (,3) +- >: inversion (repeatable) + - future: will be moved from track commands to chord parser +- <: lower inversion (repeatable) + - future: will be moved from track commands to chord parser +- ~: vibrato and pitch wheel +- `: mod wheel +- ": repeat last cell (ignoring dots, blanks, mutes, modified repeats don't repeat) +- *: set note length + - defaults to one beat when used (default is hold until mute) + - repeating symbol doubles note length + - add a number for multiply percentage (*50) +- .: half note length + - halfs note value with each dot + - add extra dot for using w/o note event (i.e. during arpeggiator), since lone dots dont mean anything + - add a number to do multiplies (i.e. C.2) +- !: accent a note (or set velocity) + - set velocity by provided percentage + - !! for louder notes + - !! for louder accent + - !! w/ number set future velocity +- ?: play note quietly (or set velocity) + - repeat or pass value for quieter notes +- T: tuplet: triplets by default, provide ratio A:B for subdivisions +- ): delay: set note delay +- \: bend: (not yet implemented) +- &: arpeggio: plays the given chord in a sequence + - infinite sequence unless number given + - more params coming soon +- $: strum + - plays the chord in a sequence, held by default + - notes automatically fit into 1 grid beat +- `: mod +- ch: assign track to a midi channel + - midi channels exceeding max value will be spanned across outputs +- p: program assign + - Set program to a given number + - Global var (%) p is usually preferred for string matching +- c: control change (midi CC param) + - setting CC5 to 25 would be c5:25 +- q: play recording +- Q: record +- midi cc mappings + - bs: bank select (not impl) + - at: aftertouch + - bc: breath controller + - fc: foot controller + - pt: portamento time + - v: volume + - bl: balance + - pn: pan + - e: expression + - ga: general purpose CC 16 + - gb: " 17 + - gc: " 18 + - gd: " 19 + - sp: sustain pedal + - ps: portamento switch + - st: sostenuto pedal + - sf: soft pedal + - lg: legato pedal + - hd: hold w/ release fade + - o: oscillator + - R: resonance + - r: release + - a: attack + - f: filter + - sa: sound ctrl + - sb: " 2 + - sc: " 3 + - sd: " 4 + - se: " 5 + - pa: portmento amount + - rv: reverb + - tr: tremolo + - cr: chorus + - ph: phaser + - mo: mono + +Track commands that start with letters should be separated +from notedata by prefixing '@': +Example: 1~ is fine, but 1v is not. Use 1@v You only need one to combine: 1@v5e5 + +Note: Fractional values specified are formatted like numbers after a decimal point: +Example: 3, 30, and 300 all mean 30% (read like .3, .30, etc.) + +CC mapping is customizable inside [def/cc.yaml](https://github.com/flipcoder/textbeat/blob/master/textbeat/def/default.yaml). + +``` + +## Scales, Modes, Chords, Voicings + ``` -b7maj7#4/sus2/1 -# same thing with note names: Bbmaj7#4/Csus2/C -# suffix this with & to hear the notes walked individually +# note: some of these features are not finished + +- < or >: inversion suffix + - ex: maj> means maj 1st inversion + - repeatable (ex. maj>> means 2nd inversion: 5 1' 3' or G C' E') + - or specify a number (like maj>2), meaning 2nd inversion (this will be useful for scale modes later) +- /: slash: layer chords across octaves (note: different from music theory interpretation) + - repeat slash for multiple octaves (ex. maj//1) +- add (suffix), add note to chord (ex. maj7add11) +- no (suffix): remove a note by number +- |: stack: combines chords/notes manually (ex. maj|sus|#11) ``` -The above chord voicing spans 3 octaves and contains 9 notes. -It is a Bbmaj7 chord w/ an added #4 (relative to Bb, which is E), followed by a lower octave Csus2. -Then at the bottom, there is a C bass note. -As cryptic as it may seem to non-musicians, condensed chord voicings are going -to make more sense to your ear over time than seeing random note letters fly by. -To build an association between the chords and how they sound, -you can type these directly into the decadence shell. - -I'm a fan of thinking about music and chords in a relative way that is not dependent on key. -For this reason, decadence prefers relative note numbers/names -over arbitrary note names (even though both are valid). -If you're writing a song in D minor, you will want to set the global or track key -to D, making D note 1. (You could also set D to 6 if you're thinking modally) -If this is confusing or not beneficial to you: don't worry, it's optional! +## Defs + +A majority of the music index is contained in inside these files: + +- Default: [def/default.yaml](https://github.com/flipcoder/textbeat/blob/master/textbeatdef/default.yaml). +- Informal: [def/informal.yaml](https://github.com/flipcoder/textbeat/blob/master/textbeat/def/informal.yaml). +- Experimental: [def/exp.yaml](https://github.com/flipcoder/textbeat/blob/master/textbeat/def/exp.yaml). + +These lists does not include certain chord modifications (add, no, drop, etc.). + +# Plugins + +You may notice there are some incomplete module/plugin systems for integration +with different sound outputs and instruments hosts. +These plugins are not yet functional. # What else? @@ -529,7 +732,7 @@ I'm improving this faster than I'm documenting it. Because of that, not everyth Check out the project board for more information on current/upcoming features. -Also, check out the basic text examples in the test/ folder. +Also, check out the basic examples in the examples/ and tests/ folder. # What's the plan? @@ -543,7 +746,7 @@ Things I'm planning on adding soon: - MIDI input/output - MIDI stabilization - Headless VST rack integration -- Csound and Sonic Pi instrument integration +- Csound and supercollider instrument integration - libGME for classic chiptune - Text-to-speech and singing (Espeak/Festival) ``` @@ -554,7 +757,7 @@ Features I'm adding eventually: - Recording and encoding output of a project - Midi controller input and recording - Midi input chord analysis -- MPE support for temperment and dynamic tonality +- MPE support for temperament and dynamic tonality ``` I'll be making use of python's multiprocessing or @@ -563,5 +766,5 @@ without doing a C++ rewrite. # Can I Help? -Yes! Join the [chat](https://gitter.im/flipcoder/decadence) or contact [flipcoder](https://github.com/flipcoder). +Yes! Contact [flipcoder](https://github.com/flipcoder). diff --git a/config/gm.yaml b/config/gm.yaml deleted file mode 100644 index 5c91c32..0000000 --- a/config/gm.yaml +++ /dev/null @@ -1,128 +0,0 @@ -- Acoustic Grand Piano -- Bright Acoustic Piano -- Electric Grand Piano -- Honky-tonk Piano -- Electric Piano 1 -- Electric Piano 2 -- Harpsichord -- Clavi -- Celesta -- Glockenspiel -- Music Box -- Vibraphone -- Marimba -- Xylophone -- Tubular Bells -- Dulcimer -- Drawbar Organ -- Percussive Organ -- Rock Organ -- Church Organ -- Reed Organ -- Accordion -- Harmonica -- Tango Accordion -- Acoustic Guitar (nylon) -- Acoustic Guitar (steel) -- Electric Guitar (jazz) -- Electric Guitar (clean) -- Electric Guitar (muted) -- Overdriven Guitar -- Distortion Guitar -- Guitar harmonics -- Acoustic Bass -- Electric Bass (finger) -- Electric Bass (pick) -- Fretless Bass -- Slap Bass 1 -- Slap Bass 2 -- Synth Bass 1 -- Synth Bass 2 -- Violin -- Viola -- Cello -- Contrabass -- Tremolo Strings -- Pizzicato Strings -- Orchestral Harp -- Timpani -- String Ensemble 1 -- String Ensemble 2 -- SynthStrings 1 -- SynthStrings 2 -- Choir Aahs -- Voice Oohs -- Synth Voice -- Orchestra Hit -- Trumpet -- Trombone -- Tuba -- Muted Trumpet -- French Horn -- Brass Section -- SynthBrass 1 -- SynthBrass 2 -- Soprano Sax -- Alto Sax -- Tenor Sax -- Baritone Sax -- Oboe -- English Horn -- Bassoon -- Clarinet -- Piccolo -- Flute -- Recorder -- Pan Flute -- Blown Bottle -- Shakuhachi -- Whistle -- Ocarina -- Lead 1 (square) -- Lead 2 (sawtooth) -- Lead 3 (calliope) -- Lead 4 (chiff) -- Lead 5 (charang) -- Lead 6 (voice) -- Lead 7 (fifths) -- Lead 8 (bass + lead) -- Pad 1 (new age) -- Pad 2 (warm) -- Pad 3 (polysynth) -- Pad 4 (choir) -- Pad 5 (bowed) -- Pad 6 (metallic) -- Pad 7 (halo) -- Pad 8 (sweep) -- FX 1 (rain) -- FX 2 (soundtrack) -- FX 3 (crystal) -- FX 4 (atmosphere) -- FX 5 (brightness) -- FX 6 (goblins) -- FX 7 (echoes) -- FX 8 (sci-fi) -- Sitar -- Banjo -- Shamisen -- Koto -- Kalimba -- Bag pipe -- Fiddle -- Shanai -- Tinkle Bell -- Agogo -- Steel Drums -- Woodblock -- Taiko Drum -- Melodic Tom -- Synth Drum -- Reverse Cymbal -- Guitar Fret Noise -- Breath Noise -- Seashore -- Bird Tweet -- Telephone Ring -- Helicopter -- Applause -- Gunshot diff --git a/decadence.py b/decadence.py deleted file mode 100755 index 2eb584a..0000000 --- a/decadence.py +++ /dev/null @@ -1,1513 +0,0 @@ -#!/usr/bin/python -"""decadence -Copyright (c) 2018 Grady O'Connell -Open-source under MIT License - -Examples: - decadence.py shell - decadence.py song.dc play song - -Usage: - decadence.py [--ring | --follow | --csound | --sonic-pi] [-eftnpsrxT] [SONGNAME] - decadence.py [+RANGE] [--ring || --follow | --csound | --sonic-pi] [-eftnpsrxT] [SONGNAME] - decadence.py -c [COMMANDS ...] - decadence.py -l [LINE_CONTENT ...] - -Options: - -h --help show this - -v --verbose verbose - -t --tempo= (STUB) set tempo - -x --grid= (STUB) set grid - -n --note= (STUB) set grid using note value - -s --speed= (STUB) playback speed - --dev= output device, partial match - -p --patch= (STUB) default midi patch, partial match - -c execute commands sequentially - -l execute commands simultaenously - -r --remote (STUB) remote, keep alive as daemon - --ring don't mute midi on end - + play from line or maker, for range use start:end - -e --edit (STUB) open file in editor - --vi (STUB) shell vi mode - -T --transpose (STUB) transpose (in half steps) - --sustain sustain by default - --numbers use note numbers in output - --notenames use note names in output - --flats prefer flats in output - --sharps prefer sharps in output - --lint (STUB) analyze file - --follow (old) print newlines every line, no output - --quiet no output - --csound (STUB) enable csound - --sonic-pi (STUB) enable sonic-pi -""" -from __future__ import unicode_literals, print_function, generators -from src import * -if __name__!='__main__': - sys.exit(0) -ARGS = docopt(__doc__) -set_args(ARGS) - -from src.support import * - -style = style_from_dict({ - Token: '#ff0066', - Token.DC: '#00aa00', - Token.Info: '#000088', -}) -colorama.init(autoreset=True) - -# logging.basicConfig(filename=LOG_FN,level=logging.DEBUG) - -dc = Context() - -# class Marker: -# def __init__(self,name,row): -# self.name = name -# self.line = row - -for arg,val in iteritems(ARGS): - if val: - if arg == '--tempo': dc.tempo = float(val) - elif arg == '--grid': dc.grid = float(val) - elif arg == '--note': dc.grid = float(val)/4.0 - elif arg == '--speed': dc.speed = float(val) - elif arg == '--verbose': dc.showtext = True - elif arg == '--dev': dc.portname = val - elif arg == '--vi': dc.vimode = True - elif arg == '--patch': - vals = val.split(',') - for i in range(len(vals)): - val = vals[i] - if val.isdigit(): - dc.tracks[i].patch(int(val)) - else: - dc.tracks[i].patch(val) - elif arg == '--sustain': dc.sustain=True - elif arg == '--ring': dc.ring=True - elif arg == '--remote': dc.daemon = True - elif arg == '--lint': LINT = True - elif arg == '--quiet': set_print(False) - elif arg == '--follow': - set_print(True) - dc.canfollow = False - elif arg == '--flats': FLATS = True - elif arg == '--sharps': SHARPS= True - elif arg == '--edit': pass - elif arg == '-l' and val: dc.dcmode = 'l' - elif arg == '-c' and val: dc.dcmode = 'c' - -if dc.dcmode=='l': - dc.buf = ' '.join(ARGS['LINE_CONTENT']).split(';') # ; -elif dc.dcmode=='c': - dc.buf = ' '.join(ARGS['COMMANDS']).split(' ') # spaces -else: # mode n - # if len(sys.argv)>=2: - # FN = sys.argv[-1] - if ARGS['SONGNAME']: - FN = ARGS['SONGNAME'] - with open(FN) as f: - for line in f.readlines(): - lc = 0 - if line: - if line[-1] == '\n': - line = line[:-1] - elif len(line)>2 and line[-2:0] == '\r\n': - line = line[:-2] - - # if not line: - # lc += 1 - # continue - ls = line.strip() - - # place marker - if ls.startswith(':'): - bm = ls[1:] - # only store INITIAL marker positions - if not bm in dc.markers: - dc.markers[bm] = lc - elif ls.startswith('|') and ls.endswith(':'): - bm = ls[1:-1] - # only store INITIAL marker positions - if not bm in dc.markers: - dc.markers[bm] = lc - - lc += 1 - dc.buf += [line] - dc.shell = False - else: - if dc.dcmode == 'n': - dc.dcmode = '' - dc.shell = True - -pygame.midi.init() -if pygame.midi.get_count()==0: - print('No midi devices found.') - sys.exit(1) -dev = -1 -for i in range(pygame.midi.get_count()): - port = pygame.midi.get_device_info(i) - portname = port[1].decode('utf-8') - # timidity - devs = [ - 'timidity port 0', - 'synth input port', - 'loopmidi' - # helm will autoconnect - ] - if dc.portname: - if portname.lower().startswith(dc.portname.lower()): - dc.portname = portname - dev = i - break - for name in devs: - if portname.lower().startswith(name): - dc.portname = portname - dev = i - break - -# dc.player = pygame.midi.Output(pygame.midi.get_default_output_id()) - -dc.player = pygame.midi.Output(dev) -dc.instrument = 0 -dc.player.set_instrument(0) -mch = 0 -for i in range(NUM_CHANNELS_PER_DEVICE): - # log("%s -> %s" % (i,mch)) - dc.tracks.append(Track(dc, i, mch, dc.player, dc.schedule)) - mch += 2 if i==DRUM_CHANNEL else 1 - -if dc.sustain: - dc.tracks[0].sustain = dc.sustain - -# show nice output in certain modes -if dc.shell or dc.dcmode in 'cl': - dc.showtext = True - -for i in range(len(sys.argv)): - arg = sys.argv[i] - - # play range (+ param, comma-separated start and end) - if arg.startswith('+'): - vals = arg[1:].split(',') - try: - dc.row = int(vals[0]) - except ValueError: - try: - dc.row = dc.markers[vals[0]] - except KeyError: - log('invalid entry point') - dc.quitflag = True - try: - dc.stoprow = int(vals[1]) - except ValueError: - try: - # we cannot cut buf now, since seq might be non-linear - dc.stoprow = dc.markers[vals[0]] - except KeyError: - log('invalid stop point') - dc.quitflag = True - except IndexError: - pass # no stop param - -if dc.shell: - log(FG.BLUE + 'decadence v'+str(VERSION)) - log('Copyright (c) 2018 Grady O\'Connell') - log('https://github.com/flipcoder/decadence') - active = SUPPORT_ALL & SUPPORT - inactive = SUPPORT_ALL - SUPPORT - if active: - log(FG.GREEN + 'Active Modules: ' + FG.WHITE + ', '.join(active) + FG.WHITE) - if inactive: - log(FG.RED + 'Inactive Modules: ' + FG.WHITE + ', '.join(inactive)) - if dc.portname: - log(FG.GREEN + 'Device: ' + FG.WHITE + '%s' % (dc.portname if dc.portname else 'Unknown',)) - if dc.tracks[0].midich == DRUM_CHANNEL: - log(FG.GREEN + 'GM Percussion') - else: - log(FG.GREEN + 'GM Patch: '+ FG.WHITE +'%s' % GM[dc.tracks[0].patch_num]) - log('Use -h for command line options.') - log('Read the manual and look at examples. Have fun!') - log('') - -header = True # set this to false as we reached cell data -while not dc.quitflag: - try: - dc.line = '.' - try: - dc.line = dc.buf[dc.row] - if dc.stoprow!=-1 and dc.row == dc.stoprow: - dc.buf = [] - raise IndexError - except IndexError: - dc.row = len(dc.buf) - # done with file, finish playing some stuff - - arps_remaining = 0 - if dc.shell or dc.daemon or dc.dcmode in ['c','l']: # finish arps in shell mode - for ch in dc.tracks[:dc.tracks_active]: - if ch.arp_enabled: - if ch.arp_cycle_limit or not ch.arp_once: - arps_remaining += 1 - dc.line = '.' - if not arps_remaining and not dc.shell and dc.dcmode not in ['c','l']: - break - dc.line = '.' - - if not arps_remaining and not dc.schedule.pending(): - if dc.shell or dc.daemon: - for ch in dc.tracks[:dc.tracks_active]: - ch.release_all() - - if dc.shell: - # dc.shell PROMPT - # log(orr(dc.tracks[0].scale,dc.scale).mode_name(orr(dc.tracks[0].mode,dc.mode))) - cur_oct = dc.tracks[0].octave - # cline = FG.GREEN + 'DC> '+FG.BLUE+ '('+str(int(dc.tempo))+'bpm x'+str(int(dc.grid))+' '+\ - # note_name(dc.tracks[0].transpose) + ' ' +\ - # orr(dc.tracks[0].scale,dc.scale).mode_name(orr(dc.tracks[0].mode,dc.mode,-1))+\ - # ')> ' - cline = 'DC> ('+str(int(dc.tempo))+'bpm x'+str(int(dc.grid))+' '+\ - note_name(dc.tracks[0].transpose) + ' ' +\ - orr(dc.tracks[0].scale,dc.scale).mode_name(orr(dc.tracks[0].mode,dc.mode,-1))+\ - ')> ' - # if bufline.endswith('.dc'): - # play file? - # bufline = raw_input(cline) - bufline = prompt(cline, - history=HISTORY, vi_mode=dc.vimode) - bufline = list(filter(None, bufline.split(' '))) - bufline = list(map(lambda b: b.replace(';',' '), bufline)) - dc.buf += bufline - elif dc.daemon: - pass - # wait on socket - continue - - else: - break - - log(FG.MAGENTA + dc.line) - - # cells = line.split(' '*2) - - # if line.startswith('|'): - # dc.separators = [] # clear - # # column setup! - # for i in range(1,len(line)): - # if line[i]=='|': - # dc.separators.append(i) - - # log(BG.RED + line) - fullline = dc.line[:] - dc.line = dc.line.strip() - - # LINE COMMANDS - ctrl = False - cells = [] - - if dc.line: - # COMMENTS (;) - if dc.line[0] == ';': - dc.follow(1) - dc.row += 1 - continue - - # set marker - if dc.line[-1]==':': # suffix marker - # allow override of markers in case of reuse - dc.markers[dc.line[:-1]] = dc.row - dc.follow(1) - dc.row += 1 - continue - # continue - elif dc.line[0]==':': #prefix marker - # allow override of markers in case of reuse - dc.markers[dc.line[1:]] = dc.row - dc.follow(1) - dc.row += 1 - continue - - # TODO: global 'silent' commands (doesn't take time) - if dc.line.startswith('%'): - dc.line = dc.line[1:].strip() # remove % and spaces - for tok in dc.line.split(' '): - if not tok: - break - if tok[0]==' ': - tok = tok[1:] - var = tok[0].upper() - if var in 'TGNPSRMCX': - cmd = tok.split(' ')[0] - op = cmd[1] - try: - val = cmd[2:] - except: - val = '' - # log("op val %s %s" % (op,val)) - if op == ':': op = '=' - if not op in '*/=-+': - # implicit = - val = str(op) + str(val) - op='=' - if not val or op=='.': - val = op + val # append - # TODO: add numbers after dots like other ops - if val[0]=='.': - note_value(val) - ct = count_seq(val) - val = pow(0.5,count) - op = '/' - num,ct = peel_uint(val[:ct]) - elif val[0]=='*': - op = '*' - val = pow(2.0,count_seq(val)) - if op=='/': - if var=='G': dc.grid/=float(val) - elif var=='X': dc.grid/=float(val) - elif var=='N': dc.grid/=float(val) #! - elif var=='T': dc.tempo/=float(val) - elif op=='*': - if var=='G': dc.grid*=float(val) - elif var=='X': dc.grid*=float(val) - elif var=='N': dc.grid*=float(val) #! - elif var=='T': dc.tempo*=float(val) - elif op=='=': - if var=='G': dc.grid=float(val) - elif var=='X': dc.grid=float(val) - elif var=='N': dc.grid=float(val)/4.0 #! - elif var=='T': - vals = val.split('x') - dc.tempo=float(vals[0]) - try: - dc.grid = float(vals[1]) - except: - pass - elif var=='C': - vals = val.split(',') - dc.columns = int(vals[0]) - try: - dc.column_shift = int(vals[1]) - except: - pass - elif var=='P': - vals = val.split(',') - for i in range(len(vals)): - p = vals[i] - if p.strip().isdigit(): - dc.tracks[i].patch(int(p)) - else: - dc.tracks[i].patch(p) - elif var=='F': # flags - for i in range(len(vals)): - dc.tracks[i].add_flags(val.split(',')) - elif var=='R' or var=='S': - try: - if val: - val = val.lower() - # ambigous alts - - if val.isdigit(): - modescale = (dc.scale.name,int(val)) - else: - alts = {'major':'ionian','minor':'aeolian'} - try: - modescale = (alts[modescale[0]],modescale[1]) - except: - pass - val = val.lower().replace(' ','') - - try: - modescale = MODES[val] - except: - raise NoSuchScale() - - try: - dc.scale = SCALES[modescale[0]] - dc.mode = modescale[1] - inter = dc.scale.intervals - dc.transpose = 0 - - log(dc.mode-1) - if var=='R': - for i in range(dc.mode-1): - inc = 0 - try: - inc = int(inter[i]) - except ValueError: - pass - dc.transpose += inc - except ValueError: - raise NoSuchScale() - else: - dc.transpose = 0 - - except NoSuchScale: - print(FG.RED + 'No such scale.') - pass - - dc.follow(1) - dc.row += 1 - continue - - # jumps - if dc.line.startswith(':') and dc.line.endswith("|"): - jumpline = dc.line[1:-1] - else: - jumpline = dc.line[1:] - if dc.line[0]=='@': - if len(jumpline)==0: - dc.row = 0 - continue - if len(jumpline)>=1 and jumpline == '@': # @@ return/pop callstack - frame = CALLSTACK[-1] - CALLSTACK = CALLSTACK[:-1] - dc.row = frame.row - continue - jumpline = jumpline.split('*') # * repeats - bm = jumpline[0] # marker name - count = 0 - if len(jumpline)>=1: - count = int(jumpline) if len(jumpline)>=1 else 1 - frame = CALLSTACK[-1] - frame.count = count - if count: # repeats remaining - CALLSTACK.append(StackFrame(dc.row)) - dc.row = dc.markers[bm] - continue - else: - dc.row = dc.markers[bm] - continue - - - # this is not indented in blank lines because even blank lines have this logic - gutter = '' - if dc.shell: - cells = list(filter(None,dc.line.split(' '))) - elif dc.columns: - cells = fullline - # shift column pos right if any - cells = ' ' * max(0,-dc.column_shift) + cells - # shift columns right, creating left-hand gutter - # cells = cells[-1*min(0,dc.column_shift):] # create gutter (if negative shift) - # separate into chunks based on column width - cells = [cells[i:i + dc.columns] for i in range(0, len(cells), dc.columns)] - # log(cells) - elif not dc.separators: - # AUTOGENERATE CELL dc.separators - cells = fullline.split(' ') - pos = 0 - for cell in cells: - if cell: - if pos: - dc.separators.append(pos) - # log(cell) - pos += len(cell) + 1 - # log( "dc.separators " + str(dc.separators)) - cells = list(filter(None,cells)) - # if fullline.startswith(' '): - # cells = ['.'] + cells # dont filter first one - autoseparate = True - else: - # SPLIT BASED ON dc.separators - s = 0 - seplen = len(dc.separators) - # log(seplen) - pos = 0 - for i in range(seplen): - cells.append(fullline[pos:dc.separators[i]].strip()) - pos = dc.separators[i] - lastcell = fullline[pos:].strip() - if lastcell: cells.append(lastcell) - - # make sure active tracks get empty cell - len_cells = len(cells) - if len_cells > dc.tracks_active: - dc.tracks_active = len_cells - else: - # add empty cells for active tracks to the right - cells += ['.'] * (len_cells - dc.tracks_active) - del len_cells - - cell_idx = 0 - - # CELL LOGIC - for cell in cells: - - cell = cells[cell_idx] - ch = dc.tracks[cell_idx] - fullcell = cell[:] - ignore = False - - # if dc.instrument != ch.instrument: - # dc.player.set_instrument(ch.instrument) - # dc.instrument = ch.instrument - - cell = cell.strip() - if cell: - header = False - - if cell.count('\"') == 1: # " is recall, but multiple " means speak - cell = cell.replace("\"", dc.track_history[cell_idx]) - else: - dc.track_history[cell_idx] = cell - - fullcell_sub = cell[:] - - # empty - # if not cell: - # cell_idx += 1 - # continue - - if cell and cell[0]=='-': - if dc.shell: - ch.mute() - else: - ch.release_all() # don't mute sustain - cell_idx += 1 - continue - - if cell and cell[0]=='=': # hard mute - ch.mute() - cell_idx += 1 - continue - - if cell and cell[0]=='-': # mute prefix - ch.release_all(True) - # ch.sustain = False - cell = cell[1:] - - notecount = len(ch.scale.intervals if ch.scale else dc.scale.intervals) - # octave = int(cell[0]) / notecount - c = cell[0] if cell else '' - - # PROCESS NOTE - chord_notes = [] # notes to process from chord - notes = [] # outgoing notes to midi - slashnotes = [[]] # using slashchords, to join this into notes [] above - allnotes = [] # same, but includes all scheduled notes - accidentals = False - # loop = True - noteloop = True - expanded = False # inside chord? if so, don't advance cell itr - events = [] - inversion = 1 # chord inversion - flip_inversion = False - inverted = 0 # notes pending inversion - chord_root = 1 - chord_note_count = 0 # include root - chord_note_index = -1 - octave = ch.octave - strum = 0.0 - noteletter = '' # track this just in case (can include I and V) - chordname = '' - chordnames = [] - - cell_before_slash=cell[:] - sz_before_slash=len(cell) - slash = cell.split('/') # slash chords - # log(slash) - tok = slash[0] - cell = slash[0][:] - slashidx = 0 - addbottom = False # add note at bottom instead - # slash = cell[0:min(cell.find(n) for n in '/|')] - - # chordnameslist = [] - # chordnoteslist = [] - # chordrootslist = [] - - while True: - n = 1 - roman = 0 # -1 lower, 0 none, 1 upper, - accidentals = '' - # number_notes = False - - # if not chord_notes: # processing cell note - # pass - # else: # looping notes of a chord? - - if tok and not tok=='.': - - # sharps/flats before note number/name - c = tok[0] - if c=='b' or c=='#': - if len(tok) > 2 and tok[0:2] =='bb': - accidentals = 'bb' - n -= 2 - tok = tok[2:] - if not expanded: cell = cell[2:] - elif c =='b': - accidentals = 'b' - n -= 1 - tok = tok[1:] - if not expanded: cell = cell[1:] - elif len(tok) > 2 and tok[0:2] =='##': - accidentals = '##' - n += 2 - tok = tok[2:] - if not expanded: cell = cell[2:] - elif c =='#': - accidentals = '#' - n += 1 - tok = tok[1:] - if not expanded: cell = cell[1:] - - # try to get roman numberal or number - c,ct = peel_roman_s(tok) - ambiguous = 0 - for amb in ('ion','dor','dom','alt','dou'): # I dim or D dim conflict w/ ionian and dorian - ambiguous += tok.lower().startswith(amb) - if ct and not ambiguous: - lower = (c.lower()==c) - c = ['','i','ii','iii','iv','v','vi','vii','viii','ix','x','xi','xii'].index(c.lower()) - noteletter = note_name(c-1,NOTENAMES,FLATS) - roman = -1 if lower else 1 - else: - # use normal numbered note - num,ct = peel_int(tok) - c = num - - # couldn't get it, set c back to char - if not ct: - c = tok[0] if tok else '' - - if c=='.': - tok = tok[1:] - cell = cell[1:] - - # tok2l = tok.lower() - # if tok2l in SOLEGE_NOTES or tok2l.startswith('sol'): - # # SOLFEGE_NOTES = - # pass - - # note numbers, roman, numerals or solege - lt = len(tok) - if ct: - c = int(c) - if c == 0: - ignore = True - break - # n = 1 - # break - # numbered notation - # wrap notes into 1-7 range before scale lookup - wrap = ((c-1) // notecount) - note = ((c-1) % notecount)+1 - # log('note ' + str(note)) - - for i in range(1,note): - # dont use scale for expanded chord notes - if expanded: - try: - n += int(DIATONIC.intervals[i-1]) - except ValueError: - n += 1 # passing tone - else: - m = orr(ch.mode,dc.mode,-1)-1 - steps = orr(ch.scale,dc.scale).intervals - idx = steps[(i-1 + m) % notecount] - n += int(idx) - if inverted: # inverted counter - if flip_inversion: - # log((chord_note_count-1)-inverted) - inverted -= 1 - else: - # log('inversion?') - # log(n) - n += 12 - inverted -= 1 - assert inversion != 0 - if inversion!=1: - if flip_inversion: # not working yet - # log('note ' + str(note)) - # log('down inv: %s' % (inversion/chord_note_count+1)) - # n -= 12 * (inversion/chord_note_count+1) - pass - else: - # log('inv: %s' % (inversion/chord_note_count)) - n += 12 * (inversion/chord_note_count) - # log('---') - # log(n) - # log('n slash %s,%s' %(n,slashidx)) - n += 12 * (wrap - slashidx) - - # log(tok) - tok = tok[ct:] - if not expanded: cell = cell[ct:] - - # number_notes = not roman - - if tok and tok[0]==':': # currently broken? wrong notes - tok = tok[1:] # allow chord sep - if not expanded: cell = cell[1:] - - # log('note: %s' % n) - - # NOTE LETTERS - elif c.upper() in '#ABCDEFG' and not ambiguous: - - n = 0 - # flats, sharps after note names? - # if tok: - if lt >= 3 and tok[1:3] =='bb': - accidentals = 'bb' - n -= 2 - tok = tok[0] + tok[3:] - cell = cell[0:1] + cell[3:] - elif lt >= 2 and tok[1] == 'b': - accidentals = 'b' - n -= 1 - tok = tok[0] + tok[2:] - if not expanded: cell = cell[0] + cell[2:] - elif lt >= 3 and tok[1:3] =='##': - accidentals = '##' - n += 2 - tok = tok[0] + tok[3:] - cell = cell[0:1] + cell[3:] - elif lt >= 2 and tok[1] =='#': - accidentals = '#' - n += 1 - tok = tok[0] + tok[2:] - if not expanded: cell = cell[0] + cell[2:] - # accidentals = True # dont need this - - if not tok: - c = 'b' # b note was falsely interpreted as flat - - # note names, don't use these in chord defn - try: - # dont allow lower case, since 'b' means flat - note = ' CDEFGAB'.index(c.upper()) - noteletter = str(c) - for i in range(note): - n += int(DIATONIC.intervals[i-1]) - n -= slashidx*12 - # adjust B(7) and A(6) below C, based on accidentials - nn = (n-1)%12+1 # n normalized - if (8<=nn<=9 and accidentals.startswith('b')): # Ab or Abb - n -= 12 - elif nn == 10 and not accidentals: - n -= 12 - elif nn > 10: - n -= 12 - tok = tok[1:] - if not expanded: cell = cell[1:] - except ValueError: - ignore = True - else: - ignore = True # reenable if there's a chord listed - - # CHORDS - is_chord = False - if not expanded: - if tok or roman: - # log(tok) - cut = 0 - nonotes = [] - chordname = '' - reverse = False - addhigherroot = False - - # cut chord name from text after it - for char in tok: - if cut==0 and char in CCHAR_START: - break - if char in CCHAR: - break - if char == '\\': - reverse = True - break - if char == '^': - addhigherroot = True - break - chordname += char - addnotes = [] - try: - # TODO: addchords - - # TODO note removal (Maj7no5) - if chordname[-2:]=='no': - numberpart = tok[cut+1:] - # second check will throws - if numberpart in '#b' or (int(numberpart) or True): - # if tok[] - prefix,ct = peel_any(tok[cut:],'#b') - if ct: cut += ct - - num,ct = peel_uint(tok[cut+1:]) - if ct: - cut += ct - cut -= 2 # remove "no" - chordname = chordname[:-2] # cut "no - nonotes.append(str(prefix)+str(num)) # ex: b5 - break - - if 'add' in chordname: - addtoks = chordname.split('add') - chordname = addtoks[0] - addnotes = addtoks[1:] - except IndexError: - log('chordname length ' + str(len(chordname))) - pass # chordname length - except ValueError: - log('bad cast ' + char) - pass # bad int(char) - cut += 1 - i += 1 - # else: - # try: - # if tok[cut+1]==AMBIGUOUS_CHORDS[chordname]: - # continue # read ahead to disambiguate - # except: - # break - - # try: - # # number chords w/o note letters aren't chords - # if int(chordname) and not noteletter: - # chordname = '' # reject - # except: - # pass - - # log(chordname) - # don't include tuplet in chordname - if chordname.endswith('T'): - chordname = chordname[:-1] - cut -= 1 - - # log(chordname) - if roman: # roman chordnames are sometimes empty - if chordname and not chordname[1:] in 'bcdef': - if roman == -1: # minor - if chordname[0] in '6719': - chordname = 'm' + chordname - else: - chordname = 'maj' if roman>0 else 'm' + chordname - - if chordname: - # log(chordname) - if chordname in BAD_CHORDS: - # certain chords may parse wrong w/ note letters - # example: aug, in this case, 'ug' is the bad chord name - chordname = noteletter + chordname # fix it - n -= 1 # fix chord letter - - # letter inversions deprecated (use <>) - # try: - # inv_letter = ' abcdef'.index(chordname[-1]) - - # # num,ct = peel_int(tok[cut+1:]) - # # if ct and num!=0: - # # cut += ct + 1 - # if inv_letter>=1: - # inversion = max(1,inv_letter) - # inverted = max(0,inversion-1) # keep count of pending notes to invert - # # cut+=1 - # chordname = chordname[:-1] - - # except ValueError: - # pass - - try: - chord_notes = expand_chord(chordname) - chord_notes = list(filter(lambda x: x not in nonotes, chord_notes)) - chord_note_count = len(chord_notes)+1 # + 1 for root - expanded = True - tok = "" - cell = cell[cut:] - is_chord = True - except KeyError as e: - # may have grabbed a ctrl char, pop one - if len(chord_notes)>1: # can pop? - try: - chord_notes = expand_chord(chordname[:-1]) - chord_notes = list(filter(lambda x,nonotes=nonotes: x in nonotes)) - chord_note_count = len(chord_notes) # + 1 for root - expanded = True - try: - tok = tok[cut-1:] - cell = cell[cut-1:] - is_chord = True - except: - assert False - except KeyError: - log('key error') - break - else: - # noteloop = True - # assert False - # invalid chord - log(FG.RED + 'Invalid Chord: ' + chordname) - break - - if is_chord: - # assert not accidentals # accidentals with no note name? - if reverse: - chord_notes = chord_notes[::-1] + ['1'] - else: - chord_notes = ['1'] + chord_notes - - chord_notes += addnotes # TODO: sort - # slashnotes[0].append(n + chord_root - 1 - slashidx*12) - # chordnameslist.append(chordname) - # chordnoteslist.append(chord_notes) - # chordrootslist.append(chord_root) - chord_root = n - ignore = False # reenable default root if chord was w/o note name - continue - else: - # log('not chord, treat as note') - pass - # assert False # not a chord, treat as note - # break - else: # blank chord name - # log('blank chord name') - # expanded = False - pass - else: # not tok and not expanded - # log('not tok and not expanded') - pass - # else and not chord_notes: - # # last note in chord, we're done - # tok = "" - # noteloop = False - - slashnotes[0].append(n + chord_root-1) - - if expanded: - if not chord_notes: - # next chord - expanded = False - - if chord_notes: - tok = chord_notes[0] - chord_notes = chord_notes[1:] - chord_note_index += 1 - # fix negative inversions - if inversion < 0: # not yet working - # octave += inversion/chord_note_count - inversion = inversion%chord_note_count - inverted = -inverted - flip_inversion = True - - if not expanded: - inversion = 1 # chord inversion - flip_inversion = False - inverted = 0 # notes pending inversion - chord_root = 1 - chord_note_count = 0 # include root - chord_note_index = -1 - chord_note_index = -1 - # next slash chord part - flip_inversion = False - inversion = 1 - chord_notes = [] - slash = slash[1:] - if slash: - tok = slash[0] - cell = slash[0] - slashnotes = [[]] + slashnotes - else: - break - slashidx += 1 - # if expanded and not chord_notes: - # break - - notes = [i for o in slashnotes for i in o] # combine slashnotes - cell = cell_before_slash[sz_before_slash-len(cell):] - - if ignore: - allnotes = [] - notes = [] - - # save the intended notes since since scheduling may drop some - # during control phase - allnotes = notes - - # TODO: arp doesn't work if channel not visible/present, move this - if ch.arp_enabled: - if notes: # incoming notes? - # log(notes) - # interupt arp - ch.arp_stop() - else: - # continue arp - arpnext = ch.arp_next() - notes = [arpnext[0]] - delay = arpnext[1] - if not fzero(delay): - ignore = False - # schedule=True - - # if notes: - # log(notes) - - cell = cell.strip() # ignore spaces - - vel = ch.vel - mute = False - sustain = ch.sustain - - delay = 0.0 - showtext = [] - arpnotes = False - arpreverse = False - arppattern = [1] - duration = 0.0 - - # if cell and cell[0]=='|': - # if not expanded: cell = cell[1:] - - # log(cell) - - # ESPEAK / FESTIVAL support wip - # if cell.startswith('\"') and cell.count('\"')==2: - # quote = cell.find('\"',1) - # word = cell[1:quote] - # BGPIPE.send((BGCMD.SAY,str(word))) - # cell = cell[quote+1:] - # ignore = True - - notevalue = '' - while len(cell) >= 1: # recompute len before check - after = [] # after events - cl = len(cell) - # All tokens here must be listed in CCHAR - - ## + and - symbols are changed to mean minor and aug chords - # if c == '+': - # log("+") - # c = cell[1] - # shift = int(c) if c.isdigit() else 0 - # mn = n + base + (octave+shift) * 12 - c = cell[0] - c2 = None - if cl: - c2 = cell[:2] - - if c: c = c.lower() - if c2: c2 = c2.lower() - - # if c == '-' or c == '+' or c.isdigit(c): - # cell = cell[1:] # deprecated, ignore - # continue - - # OCTAVE SHIFT UP - # if sym== '>': ch.octave = octave # persist - # row_events += 1 - # elif c == '-': - # c = cell[1] - # shift = int(c) if c.isdigit() else 0 - # p = base + (octave+shift) * 12 - # INVERSION - ct = 0 - if c == '>' or c=='<': - sign = (1 if c=='>' else -1) - ct = count_seq(cell) - for i in range(ct): - if notes: - notes[i%len(notes)] += 12*sign - notes = notes[sign*1:] + notes[:1*sign] - # when used w/o note/chord, track history should update - # dc.track_history[cell_idx] = fullcell_sub - # log(notes) - if ch.arp_enabled: - ch.arp_notes = ch.arp_notes[1:] + ch.arp_notes[:1] - cell = cell[1+ct:] - elif c == ',' or c=='\'': - cell = cell[1:] - sign = 1 if c=='\'' else -1 - if cell and cell[0].isdigit(): # numbers persist - shift,ct = peel_int(cell,1) - cell = cell[ct:] - octave += sign*shift - ch.octave = octave # persist - else: - rpt = count_seq(cell,',') - octave += sign*(rpt+1) # persist - cell = cell[rpt:] - # SET OCTAVE - elif c == '=': - cell = cell[1:] - if cell and cell[0].isdigit(): - octave = int(cell[0]) - cell = cell[1:] - else: - octave = 0 # default - shift = 1 - ch.octave = octave - # row_events += 1 - # VIBRATO - elif cl>1 and cell.startswith('~'): # vib/pitch wheel - if c=='/' or c=='\\': - num,ct = peel_int_s(cell[2:]) - num *= 1 if c=='/' else -1 - cell = cell[2:] - if ct: - sign = 1 - if num<0: - num=num[1:] - sign = -1 - vel = min(127,sign*int(float('0.'+num)*127.0)) - else: - vel = min(127,int(curv + 0.5*(127.0-curv))) - cell = cell[ct+1:] - ch.pitch(vel) - elif c == '~': # pitch wheel - ch.pitch(127) - cell = cell[1:] - elif c == '`': # mod wheel - ch.mod(127) - cell = cell[1:] - # dc.sustain - elif cell.startswith('__-'): - ch.mute() - sustain = ch.sustain = True - cell = cell[3:] - elif c2=='__': - sustain = ch.sustain = True - cell = cell[2:] - elif c2=='_-': - sustain = False - cell = cell[2:] - elif c=='_': - sustain = True - cell = cell[1:] - elif cell.startswith('%v'): # volume - pass - cell = cell[2:] - # get number - num = '' - for char in cell: - if char.isdigit(): - num += char - else: - break - assert num != '' - cell = cell[len(num):] - vel = int((float(num) / float('9'*len(num)))*127) - ch.cc(7,vel) - # elif c=='v': # velocity - may be deprecated for ! - # cell = cell[1:] - # # get number - # num = '' - # for char in cell: - # if char.isdigit(): - # num += char - # else: - # break - # assert num != '' - # cell = cell[len(num):] - # vel = int((float(num) / 100)*127) - # ch.vel = vel - # # log(vel) - elif c=='cc': # MIDI CC - # get number - cell = cell[1:] - cc,ct = peel_int(cell) - assert ct - cell = cell[len(num)+1:] - ccval,ct = peel_int(cell) - assert ct - cell = cell[len(num):] - ccval = int(num) - ch.cc(cc,ccval) - elif cl>=2 and c=='pc': # program/patch change - cell = cell[2:] - p,ct = peel_int(cell) - assert ct - cell = cell[len(num):] - # ch.cc(0,p) - ch.patch(p) - elif c2=='ch': # midi channel - num,ct = peel_uint(cell[2:]) - cell = cell[2+ct:] - ch.midi_channel(num) - if dc.showtext: - showtext.append('channel') - elif c=='*': - dots = count_seq(cell) - if notes: - cell = cell[dots:] - num,ct = peel_float(cell, 1.0) - cell = cell[ct:] - if dots==1: - duration = num - events.append(Event(num, lambda _: ch.release_all(), ch)) - else: - duration = num*pow(2.0,float(dots-1)) - events.append(Event(num*pow(2.0,float(dots-1)), lambda _: ch.release_all(), ch)) - else: - cell = cell[dots:] - if dc.showtext: - showtext.append('duration(*)') - elif c=='.': - dots = count_seq(cell) - if len(c)>1 and notes: - notevalue = '.' * dots - cell = cell[dots:] - if ch.arp_enabled: - dots -= 1 - if dots: - num,ct = peel_uint_s(cell) - if ct: - num = int('0.' + num) - else: - num = 1.0 - cell = cell[ct:] - duration = num*pow(0.5,float(dots)) - events.append(Event(num*pow(0.5,float(dots)), lambda _: ch.release_all(), ch)) - else: - cell = cell[dots:] - if dc.showtext: - showtext.append('shorten(.)') - elif c=='(' or c==')': # note shift (early/delay) - num = '' - cell = cell[1:] - s,ct = peel_uint(cell, 5) - if ct: - cell = cell[ct:] - delay = -1*(c=='(')*float('0.'+num) if num else 0.5 - assert(delay > 0.0) # TOOD: impl early notes - elif c=='|': - cell = cell[1:] # ignore - elif c2=='!!': # full accent - vel,ct = peel_uint_s(cell[1:],127) - cell = cell[2+ct:] - if ct>2: - ch.vel = vel # persist if numbered - else: - if ch.max_vel >= 0: - vel = ch.max_vel - else: - vel = 127 - if dc.showtext: - showtext.append('accent(!!)') - elif c=='!': # accent - curv = ch.vel - num,ct = peel_uint_s(cell[1:]) - if ct: - vel = min(127,int(float('0.'+num)*127.0)) - else: - if ch.accent_vel >= 0: - vel = ch.accent_vel - else: - vel = min(127,int(curv + 0.5*(127.0-curv))) - cell = cell[ct+1:] - if dc.showtext: - showtext.append('accent(!!)') - elif c2=='??': # ghost - if ch.ghost_vel >= 0: - vel = ch.ghost_vel # max(0,int(ch.vel*0.25)) - else: - vel = 1 - cell = cell[2:] - if dc.showtext: - showtext.append('soften(??)') - elif c=='?': # soft - if ch.quiet_vel>0: - vel = ch.soft_vel - else: - vel = max(0,int(ch.vel*0.5)) - cell = cell[1:] - if dc.showtext: - showtext.append('soften(??)') - # elif cell.startswith('$$') or (c=='$' and lennotes==1): - elif c=='$': # strum/spread/tremolo - sq = count_seq(cell) - cell = cell[sq:] - num,ct = peel_uint_s(cell,'0') - cell = cell[ct:] - num = float('0.'+num) - strum = 1.0 - if len(notes)==1: # tremolo - notes = notes * 2 - # notes = [notes[i:i + sq] for i in range(0, len(notes), sq)] - # log('strum') - if dc.showtext: - showtext.append('strum($)') - elif c=='&': - count = count_seq(cell) - num,ct = peel_uint(cell[count:],0) - # notes = list(itertools.chain.from_iterable(itertools.repeat(\ - # x, count) for x in notes\ - # )) - cell = cell[ct+count:] - if count>1: arpreverse = True - if not notes: - # & restarts arp (if no note) - ch.arp_enabled = True - ch.arp_count = num - ch.arp_idx = 0 - else: - arpnotes = True - - if cell.startswith(':'): - num,ct = peel_uint(cell[1:],1) - arppattern = [num] - cell = cell[1+ct:] - if dc.showtext: - showtext.append('arpeggio(&)') - elif c=='t': # tuplet - if not ch.tuplets: - ch.tuplets = True - pow2i = 0.0 - cell = cell[1:] - num,ct = peel_uint(cell,'3') - cell = cell[ct:] - ct2=0 - denom = 0 - if cell and cell[0]==':': - denom,ct2 = peel_float(cell) - cell = cell[1+ct2:] - if not ct2: - for i in itertools.count(): - denom = 1 << i - if denom > num: - break - ch.note_spacing = denom/float(num) # ! - ch.tuplet_count = int(num) - cell = cell[ct:] - else: - cell = cell[1:] - pass - elif c=='@': - if not notes: - cell = [] - continue # ignore jump - # elif c==':': - # if not notes: - # cell = [] - # continue # ignore marker - elif c=='%': - # ctrl line - cell = [] - break - else: - if dc.dcmode in 'cl': - log(FG.BLUE + dc.line) - indent = ' ' * (len(fullcell)-len(cell)) - log(FG.RED + indent + "^ Unexpected " + cell[0] + " here") - cell = [] - ignore = True - break - - # elif c=='/': # bend in - # elif c=='\\': # bend down - - base = (OCTAVE_BASE+octave) * 12 - 1 + dc.transpose + ch.transpose - p = base - - if arpnotes: - ch.arp(notes, num, sustain, arppattern, arpreverse) - arpnext = ch.arp_next() - notes = [arpnext[0]] - delay = arpnext[1] - # if not fcmp(delay): - # pass - # schedule=True - - if notes: - ch.release_all() - - for ev in events: - dc.schedule.add(ev) - - delta = 0 # how much to separate notes - if strum < -EPSILON: - notes = notes[::-1] # reverse - strum -= strum - if strum > EPSILON: - ln = len(notes) - delta = (1.0/(ln*forr(duration,1.0))) #t between notes - - if dc.showtext: - # log(FG.MAGENTA + ', '.join(map(lambda n: note_name(p+n), notes))) - # chordoutput = chordname - # if chordoutput and noletter: - # coordoutput = note_name(chord_root+base) + chordoutput - # log(FG.CYAN + chordoutput + " ("+ \) - # (', '.join(map(lambda n,base=base: note_name(base+n),notes)))+")" - # log(showtext) - showtext = [] - if chordname and not ignore: - noteletter = note_name(n+base) - # for cn in chordnames: - # log(FG.CYAN + noteletter + cn + " ("+ \) - # (', '.join(map(lambda n,base=base: note_name(base+n),allnotes)))+")" - - delay += ch.tuplet_next() - - i = 0 - for n in notes: - # if no schedule, play note immediately - # also if scheduled, play first note of strum if there's no delay - if fzero(delay): - # if not schedule or (i==0 and strum>=EPSILON and delay2) - sus3: 'b3 4 5 b7' # 1->b3 ccw - sus3+: '2 3 5 6' # 1->3 cw - sus7+: '2 5 6 7 9' # 1->7 cw - sus26: '5 6 9' - sus13: '4 5 9 13' - - # edges of scale shape along circle (i.e. darkest and brightest notes of each whole tone scale) - eg: '3 4 7' # diatonic edges ("eg>>"=phyr "eg>3"=lyd "eg>6"=loc) - eg-: '2 b3 7' # melodic minor edges - arp: '3 4 5 7' # aka wu7, diatonic edge w/ 5, for inversions use '|5' eg: eg>|5 - melo: '2 b3 5 7' # eg- w/ 5 melodic minor arp - diff --git a/def/default.yaml b/def/default.yaml deleted file mode 100644 index ce50bea..0000000 --- a/def/default.yaml +++ /dev/null @@ -1,281 +0,0 @@ -scales: - diatonic: - intervals: '2212221' - modes: - - ionian - - dorian - - phyrigian - - lydian - - mixolydian - - aeolian - - locrian - chromatic: - intervals: '111111111111' - wholetone: - human: 'whole tone' - intervals: '222222' - bebop: - intervals: '2212p121' - pentatonic: - intervals: '23223' - modes: - - yo - - minorpentatonic - - majorpentatonic - - egyption - - mangong - blues: - intervals: '32p132' - melodicminor: - intervals: '2122221' - harmonicminor: - human: 'harmonic minor' - intervals: '2122132' - modes: - - harmonicminor - - locriann6 - - ionianaug#5 - - dorian#4 - - phyrigianminor - - lydian#9 - - alteredbb7 - harmonicmajor: - human: 'harmonic major' - intervals: '2122132' - modes: - - dorianb5 - - phyrgianb4 - - lydianb3 - - mixolydianb2 - - lydianaug#2 - - locrianbb7 - doubleharmonic: - human: 'double harmonic' - intervals: '1312131' - modes: - - doubleharmonic - - lydian#2#6 - - ultraphyrigian - - hungarianminor - - oriental - - ionianaug - - locrianbb3bb7 - neapolitan: - intervals: '1222221' - modes: - - neapolitan - - leadingwholetone - - lydianaugdom - - minorlydian - - arabian - - semilocrianb4 - - superlocrianbb3 - neapolitanminor: - human: 'neapolitan minor' - intervals: '222222' - modes: - - neapolitanminor - - lydian#6 - - mixolydianaug - - hungarian - - locriandom - - ionain#2 - - ultralocrianbb3 - -chords: - '1': '' - #1: '#1' - m2: 'b2' - '2': '2' - #2: '#2' - b3: 'b3' - m3: 'b3' - '3': '3' - '4': '4' - #4: '#4' - b5: 'b5' - '5': '5' - #5: '#5' - b6: 'b6' - #5: '#5' - p6: '6' - p7: '7' - b7: 'b7' - #6: '#6' - #8: '#8' - #9: '#9' - # '#11: '#11' # easy confusion with 11 chord - - # Common chords and voicings - - ma: '3 5' - mab5: '3 b5' # lyd - ma#4: '3 #4 5' # lydadd5 - ma6: '3 5 6' - ma69: '3 5 6 9' - ma769: '3 5 6 7 9' - m69: 'b3 5 6 9' - ma7: '3 5 7' - ma7#4: '3 #4 5 7' # lyd7add5 - ma7b5: '3 b5 7' # lyd7 - ma7b9: '3 5 7 b9' - ma7b13: '3 5 7 9 11 b13' - ma9: '3 5 7 9' - # 'maadd9: '3 5 9' - ma9b5: '3 b5 7 9' - ma9+: '3 #5 7 9' - ma#11: '3 b5 7 9 #11' - ma11: '3 5 7 9 11' - ma11b5: '3 b5 7 9 11' - ma11+: '3 #5 7 9 11' - ma11b13: '3 #5 7 9 11 b13' - # 'maadd11: '3 5 11' - # 'maadd#11: '3 5 #11' - ma13: '3 5 7 9 11 13' - ma13b5: '3 b5 7 9 11 13' - ma13+: '3 #5 7 9 11 13' - ma13#4: '3 #4 5 7 9 11 13' - m: 'b3 5' - m6: 'b3 5 6' - m7: 'b3 5 b7' - m69: 'b3 5 6 9' - m769: 'b3 5 6 7 9' - # 'madd9: '3 b5 9' - m7b5: 'b3 b5 b7' - m7+: 'b3 #5 b7' - m9: 'b3 5 b7 9' - m9+: 'b3 #5 b7 9' - m11: 'b3 5 b7 9 11' - m11+: 'b3 #5 b7 9 11' - m11b9: 'b3 5 b7 b9 11' - m11b13: 'b3 5 b7 9 11 b13' - m13: 'b3 5 b7 9 11 13' - m13b9: '3 5 b7 b9 11 13' - m13#9: '3 5 b7 #9 11 13' - m13b11: '3 5 b7 #9 b11 13' - m13#11: '3 5 b7 #9 #11 13' - +: '3 #5' - 7+: '3 #5 b7' - 9+: '3 #5 b7 9' - 11+: '3 #5 b7 9 11' - 13+: '3 #5 b7 9 11 13' - 'ma6': '3 5 6' - 'ma11': '3 5 b7 9 #11' - 'ma13': '3 5 7 9 11 13' - 'ma15': '3 5 b7 9 #11 #15' - '7': '3 5 b7' - 7b5: '3 b5 b7' - '69': '3 5 6 b7 9' - 7+: '3 #5 b7' - 7b9: '3 5 b7 b9' - '9': '3 5 b7 9' - 9b5: '3 b5 b7 9' - 9+: '3 #5 b7 9' - '9#11': '3 5 b7 9 #11' - '11': '3 5 b7 9 11' - 11b5: '3 b5 b7 9 11' - 11+: '3 #5 b7 9 11' - 11b9: '3 5 b7 b9 11' - '6': '3 5 6' - '9': '3 5 b7 9' - '11': '3 5 b7 9 #11' - '13': '3 5 b7 9 11 13' - 13b5: '3 b5 b7 9 11 13' - 13#11: '3 5 b7 9 #11 13' - 13+: '3 #5 b7 9 13' - '15': '3 5 b7 9 #11 #15' - dim: 'b3 b5' - dim7: 'b3 b5 bb7' - dim9: 'b3 b5 bb7 9' - dim11: 'b3 b5 bb7 9 11' - sus: '4 5' - sus2: '2 5' - - # Informal voicings below -- eventually move to another file - - f: '5' - pow: '5 8' - mm7: 'b3 5 7' - mm9: 'b3 5 7 9' - mm11: 'b3 5 7 9 11' - mm13: 'b3 5 7 9 11 13' - - # maj2 - mu: '2 3 5' - mu7: '2 3 5 7' - mu7#4: '2 3 #4 5 7' # lyd7 3sus2|sus2 - mu-: '2 b3 5' - mu-7: '2 b3 5 7' - mu-7b5: '2 3 b5 7' - mu7b5: '2 3 b5 7' - - # maj4 - wu: '3 4 5' # maadd4 - wu7: '3 4 5 7' # ma7add4 - wu7b5: '2 3 b5 7' - wu-: 'b3 4 5' # madd4 - wu-7: 'b3 4 5 7' - wu-7b5: 'b3 4 b5 7' - - # 'edge': play edges of scale shape along tone ladder (i.e. darkest and brightest notes of each whole tone scale) - eg: '3 4 7' # diatonic scale edges (egb=phyr egc=lyd egd=loc) - eg-: '2 b3 7' # melodic minor edge - arp: '3 4 5 7' # diatonic edge w/ 5 for inversions use '|5' eg: egb|5 - melo: '2 b3 5 7' # eg- w/ 5 melodic minor arp - - # extended sus i.e. play contiguous notes along circle of 5ths - sq: '2 4 5' # 'square' sus2|4 sus both directions contiguous ladder (4->2) - sus3: 'b3 4 5 b7' # 1->b3 ccw - sus3+: '2 3 5 6' # 1->3 cw - sus7: '4 5 b7' # 1->b7 ccw - sus7+: '2 5 6 7 9' # 1->7 cw - sus26: '5 6 9' - sus9: '4 5 9' - sus13: '4 5 9 13' - q: '4 b7' # quartal (stacked 4ths) sus voicing - qt: '5 9' # quintal (stacked 5ths) sus voicing - -chord_alts: - r: '1' - M2: '2' - M3: '3' - aug: + - ma#5: + - ma#5: + - aug7: +7 - p4: '4' - p5: '5' - -: m - M: ma - sus4: sus - # major: maj - ma7: ma7 - ma9: ma9 - lyd: mab5 - lyd7: ma7b5 - plyd: ma#4 - plyd7: ma7#4 - # Madd9: maadd9 - # maor7: ma7 - Mb5: mab5 - # M7: ma7 - # M7b5: ma7b5 - # min: m - # minor: m - # min7: m7 - # minor7: m7 - p: pow - # 11th: 11 - o: dim - o7: dim7 - 7o: dim7 - o9: dim9 - 9o: dim9 - o11: dim11 - 11o: dim11 - sus24: sq - # mma7: mm7 - # mma9: mm9 - # mma11: mm11 - # mma13: mm13 - diff --git a/def/informal.yaml b/def/informal.yaml deleted file mode 100644 index 5847a9d..0000000 --- a/def/informal.yaml +++ /dev/null @@ -1,17 +0,0 @@ -chords: - - # majadd2 - mu: '2 3 5' - mu7: '2 3 5 7' - mu7#4: '2 3 #4 5 7' # lyd7 3sus2|sus2 - mu-: '2 b3 5' - mu-7: '2 b3 5 7' - mu-7b5: '2 3 b5 7' - mu7b5: '2 3 b5 7' - - # jazz sus voicings - q: '4 b7' # quartal (stacked 4ths) sus voicing - qt: '5 9' # quintal (stacked 5ths) sus voicing - sus7: '4 5 b7' # 1->b7 ccw - sus9: '4 5 9' - diff --git a/examples/1.txbt b/examples/1.txbt new file mode 100644 index 0000000..85ab2d1 --- /dev/null +++ b/examples/1.txbt @@ -0,0 +1,24 @@ +%t=100x4 p=piano,bass,drums c=20,-2 + ,2 +|: +maj7__@v4 1 1@v5 + 4 b3 + 5 5 + 1 1 + b3 + 1 1 + 4 5 + 5 b3 + 1 1$ + b3 +dom4/b7, b7 1 + 4 b3 + 5 5 + b7, 1 + b3 + b7 1 + 4 5 + 5 b3 + b7, 1$ + b3 +:| diff --git a/examples/2.txbt b/examples/2.txbt new file mode 100644 index 0000000..53f245c --- /dev/null +++ b/examples/2.txbt @@ -0,0 +1,37 @@ +%t100 x1 p=piano,piano c16,-2 +ma#4/1$@v5__ 1'1 +ma#4/1$ +ma#4/1$ 7,1 +ma#4/1$ +ma#4/1$ 6 +ma#4/1$ +ma#4/1$ 5 +ma#4/1$ +2wu/2$ 6 +2wu/2$ +2wu/2$ +2wu/2$ +2wu/2$ b5 +2wu/2$ +2wu/2$ +2wu/2$ +%t100 x4 k3 +1 1,2 +b3mu7 +b3mu7 +b3mu7 + +1 +b3mu7 +b3mu7 +b3mu7 + +1 1,1 +b3mu7 +b3mu7 +b3mu7 + +1 +b3mu7 +b3mu7 +b3mu7 diff --git a/examples/3.txbt b/examples/3.txbt new file mode 100644 index 0000000..53bc9fa --- /dev/null +++ b/examples/3.txbt @@ -0,0 +1,25 @@ +%t=120x4 p=piano,bass,drums c=20,-2 + +ma. 3,1 1 + m?. + m?. +ma. + m?. 3 +ma. + m?. + m?. +ma. 1 + m?. + m?. +ma. b7 + m?. 3 + m?. +ma. + m?. +ma. 1 + m?. + m?. +ma. + m?. 3 + m?. 5 + diff --git a/examples/4.txbt b/examples/4.txbt new file mode 100644 index 0000000..a4766d2 --- /dev/null +++ b/examples/4.txbt @@ -0,0 +1,38 @@ +%t100 x4 p=electric,electric c16,-2 +%k=3 +1 ,1 +2 +b3 +4 +|: +5 3m!& + ? + ? + ! + ? + ? + ? +b6 4m!&' + ? + ? + ! + ? + ? + ? +:| +|a: + 6m/6==,2 + 6m/6.. + + 6m/6.. + + 6m/6.. +:a*2| + b7m + + b7m + + b7m + +:| + diff --git a/examples/5.txbt b/examples/5.txbt new file mode 100644 index 0000000..b7abd4e --- /dev/null +++ b/examples/5.txbt @@ -0,0 +1,88 @@ +%t120 x4 c20,-2 Ppiano,piano +__ '1 +|: +sus,@v5 3 +sus& + 1 + +sus, 2 +sus& + 5 + +5sus, 3 +5sus& + 1 + +5sus, 2 +5sus& + + +:| +|: +4sus, 4 +4sus& + + +4sus, 3 +4sus& + + +4sus, 4, +4sus& + + +4sus, +4sus& + + +:| +|: +ma, +ma& + + +ma, +ma& + + +5m, +5m& + + +5m, +5m& + + +:| +b7m, +b7m& + + +b7m, +b7m& + + +6ma, +6ma& + + +6ma, +6ma& + + +b7m, +b7m& + + +b7m, +b7m& + + +6ma, +6ma& + + +6ma, +6ma& + + diff --git a/examples/6.txbt b/examples/6.txbt new file mode 100644 index 0000000..0519788 --- /dev/null +++ b/examples/6.txbt @@ -0,0 +1,20 @@ +%t120x4 c24,-2 ppiano,piano +'2__ +|: +1/mmu7/mmu7/mmu7& + + + + + + + + + + + + + + + +:| diff --git a/examples/7.txbt b/examples/7.txbt new file mode 100644 index 0000000..be60caf --- /dev/null +++ b/examples/7.txbt @@ -0,0 +1,7 @@ +%v0 t120x2 c16,-2 ppiano,piano + + +phyrigian + + + diff --git a/examples/drums1.txbt b/examples/drums1.txbt new file mode 100644 index 0000000..c349bf6 --- /dev/null +++ b/examples/drums1.txbt @@ -0,0 +1,7 @@ +%t120 x2 c20,-2 p=drums f=loop +1 +b5$? +b5? +3 +b5$? +b5? diff --git a/test/jazz.dc b/examples/jazz.txbt similarity index 95% rename from test/jazz.dc rename to examples/jazz.txbt index d2ba289..eb1b03a 100644 --- a/test/jazz.dc +++ b/examples/jazz.txbt @@ -1,4 +1,4 @@ -%g3 t180 ppiano,bass,drums c12,-2 +%x3 t180 ppiano,bass,drums c12,-2 5dom7 5,2 1 diff --git a/test/mary.dc b/examples/mary.txbt similarity index 97% rename from test/mary.dc rename to examples/mary.txbt index 7ab0dec..73cb463 100644 --- a/test/mary.dc +++ b/examples/mary.txbt @@ -32,4 +32,4 @@ 1' -. + diff --git a/examples/metronome.txbt b/examples/metronome.txbt new file mode 100644 index 0000000..fa912ab --- /dev/null +++ b/examples/metronome.txbt @@ -0,0 +1,5 @@ +%n=8 p=drums c10,-2 f=loop +1 +b5 +3 +b5 diff --git a/icon.png b/icon.png deleted file mode 100644 index 7ef2727..0000000 Binary files a/icon.png and /dev/null differ diff --git a/requirements.txt b/requirements.txt index b692760..5f5594c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,5 @@ appdirs pyyaml docopt future +shutilwhich +mido diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ef3cb34 --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +#!/usr/bin/python +from __future__ import unicode_literals +from setuptools import setup, find_packages +import sys +if sys.version_info[0]==2: + sys.exit('Sorry, python2 support is currently broken. Use python3!') +setup( + name='textbeat', + version='0.1.0', + description='text music sequencer and midi shell', + url='https://github.com/filpcoder/textbeat', + author='Grady O\'Connell', + author_email='flipcoder@gmail.com', + license='MIT', + packages=['textbeat','textbeat.def','textbeat.presets','textbeat.plugins'], + include_package_data=True, + install_requires=[ + 'pygame','colorama','prompt_toolkit','appdirs','pyyaml','docopt','future','shutilwhich','mido' + ], + entry_points=''' + [console_scripts] + textbeat=textbeat.__main__:main + txbt=textbeat.__main__:main + ''', + zip_safe=False +) + diff --git a/src/context.py b/src/context.py deleted file mode 100644 index 2075850..0000000 --- a/src/context.py +++ /dev/null @@ -1,61 +0,0 @@ -from . import * - -class StackFrame: - def __init__(self, row): - self.row = row - self.counter = 0 # repeat call counter - -class Context: - - def __init__(self): - self.quitflag = False - self.vimode = False - self.bcproc = None - self.log = False - self.canfollow = False - self.lint = False - self.tracks_active = 1 - self.showmidi = False - self.scale = DIATONIC - self.mode = 1 - self.transpose = 0 - self.tempo = 90.0 - self.grid = 4.0 # Grid subdivisions of a beat (4 = sixteenth note) - self.columns = 0 - self.column_shift = 0 - self.showtext = True # nice output (-v), only shell and cmd modes by default - self.sustain = False # start sustained - self.ring = False # disables midi muting on program exit - self.buf = [] - self.markers = {} - self.callstack = [StackFrame(-1)] - self.schedule = [] - self.separators = [] - self.track_history = ['.'] * NUM_TRACKS - self.fn = None - self.row = 0 - self.stoprow = -1 - self.dcmode = 'n' # n normal c command s sequence - self.schedule = Schedule(self) - self.tracks = [] - self.shell = True - self.daemon = False - self.gui = False - self.portname = '' - self.speed = 1.0 - self.player = None - self.instrument = None - - def follow(self, count): - if self.canfollow: - print('\n' * max(0,count-1)) - - def pause(self): - try: - for ch in self.tracks[:self.tracks_active]: - ch.release_all(True) - input(' === PAUSED === ') - except: - return False - return True - diff --git a/src/remote.py b/src/remote.py deleted file mode 100644 index 1c050d4..0000000 --- a/src/remote.py +++ /dev/null @@ -1,3 +0,0 @@ -from . import * - - diff --git a/src/track.py b/src/track.py deleted file mode 100644 index 3bb9c38..0000000 --- a/src/track.py +++ /dev/null @@ -1,259 +0,0 @@ -from . import * - -class Track: - FLAGS = set('auto_roman') - def __init__(self, ctx, idx, midich, player, schedule): - self.idx = idx - self.ctx = ctx - # self.players = [player] - self.player = player - self.schedule = schedule - self.channels = [midich] - self.midich = midich # tracks primary midi channel - self.initial_channel = midich - self.non_drum_channel = midich - self.reset() - def reset(self): - self.notes = [0] * RANGE - self.sustain_notes = [False] * RANGE - self.mode = 0 # 0 is NONE which inherits global mode - self.scale = None - self.instrument = 0 - self.octave = 0 # rel to OCTAVE_BASE - self.modval = 0 # dont read in mod, just track its change by this channel - self.sustain = False # sustain everything? - self.arp_notes = [] # list of notes to arpegiate - self.arp_idx = 0 - self.arp_cycle_limit = 0 # cycles remaining, only if limit != 0 - self.arp_pattern = [] # relative steps to - self.arp_enabled = False - self.arp_once = False - self.arp_delay = 0.0 - self.arp_sustain = False - self.arp_note_spacing = 1.0 - self.arp_reverse = False - self.vel = 100 - self.max_vel = -1 - self.soft_vel = -1 - self.ghost_vel = -1 - self.accent_vel = -1 - self.non_drum_channel = self.initial_channel - # self.off_vel = 64 - self.staccato = False - self.patch_num = 0 - self.transpose = 0 - self.pitch = 0.0 - self.tuplets = False - self.note_spacing = 1.0 - self.tuplet_count = 0 - self.tuplet_offset = 0.0 - self.use_sustain_pedal = False # whether to use midi sustain instead of track - self.sustain_pedal_state = False # current midi pedal state - self.schedule.clear_channel(self) - self.flags = set() - # def _lazychannelfunc(self): - # # get active channel numbers - # return list(map(filter(lambda x: self.channels & x[0], [(1<0: - ch.cc(1,0) - self.modval = False - def panic(self): - for ch in self.channels: - status = (MIDI_CC<<4) + ch - if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status,123,0)) - self.player.write_short(status, 123, 0) - if self.modval>0: - ch.cc(1,0) - self.modval = False - def note_on(self, n, v=-1, sustain=False): - if self.use_sustain_pedal: - if sustain and self.sustain != sustain: - self.cc(MIDI_SUSTAIN_PEDAL, sustain) - elif not sustain: # sustain=False is overridden by track sustain - sustain = self.sustain - if v == -1: - v = self.vel - if n < 0 or n > RANGE: - return - for ch in self.channels: - self.notes[n] = v - self.sustain_notes[n] = sustain - # log("on " + str(n)) - if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: NOTE ON (%s, %s, %s)' % (n,v,ch)) - self.player.note_on(n,v,ch) - def note_off(self, n, v=-1): - if v == -1: - v = self.vel - if n < 0 or n >= RANGE: - return - if self.notes[n]: - # log("off " + str(n)) - for ch in self.channels: - if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: NOTE OFF (%s, %s, %s)' % (n,v,ch)) - self.player.note_off(n,v,ch) - self.notes[n] = 0 - self.sustain_notes[n] = 0 - self.cc(MIDI_SUSTAIN_PEDAL, True) - def release_all(self, mute_sus=False, v=-1): - if v == -1: - v = self.vel - for n in range(RANGE): - # if mute_sus, mute sustained notes too, otherwise ignore - mutesus_cond = True - if not mute_sus: - mutesus_cond = not self.sustain_notes[n] - if self.notes[n] and mutesus_cond: - for ch in self.channels: - if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: NOTE OFF (%s, %s, %s)' % (n,v,ch)) - self.player.note_off(n,v,ch) - self.notes[n] = 0 - self.sustain_notes[n] = 0 - # log("off " + str(n)) - # self.notes = [0] * RANGE - if self.modval>0: - self.cc(1,0) - # self.arp_enabled = False - self.schedule.clear_channel(self) - # def cut(self): - def midi_channel(self, midich, stackidx=-1): - if midich==DRUM_CHANNEL: # setting to drums - if self.channels[stackidx] != DRUM_CHANNEL: - self.non_drum_channel = self.channels[stackidx] - self.octave = DRUM_OCTAVE - else: - for ch in self.channels: - if ch!=DRUM_CHANNEL: - midich = ch - if midich != DRUMCHANNEL: # no suitable channel in span? - midich = self.non_drum_channel - if stackidx == -1: # all - self.release_all() - self.channels = [midich] - elif midich not in self.channels: - self.channels.append(midich) - def pitch(self, val): # [-1.0,1.0] - val = min(max(0,int((1.0 + val)*0x2000)),16384) - self.pitch = val - val2 = (val>>0x7f) - val = val&0x7f - for ch in self.channels: - status = (MIDI_PITCH<<4) + self.midich - if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: PITCH (%s, %s)' % (val,val2)) - self.player.write_short(status,val,val2) - self.mod(0) - def cc(self, cc, val): # control change - if type(val) ==type(bool): val = 127 if val else 0 # allow cc bool switches - for ch in self.channels: - status = (MIDI_CC<<4) + ch - if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status, cc,val)) - self.player.write_short(status,cc,val) - if cc==1: - self.modval = val - def mod(self, val): - return cc(1,val) - def patch(self, p, stackidx=0): - if isinstance(p,basestring): - # look up instrument string in GM - i = 0 - inst = p.replace('_',' ').replace('.',' ').lower() - - if p in DRUM_WORDS: - self.midi_channel(DRUM_CHANNEL) - p = 0 - else: - if self.midich == DRUM_CHANNEL: - self.midi_channel(self.non_drum_channel) - - stop_search = False - gmwords = GM_LOWER - for w in inst.split(' '): - gmwords = list(filter(lambda x: w in x, gmwords)) - lengw = len(gmwords) - if lengw==1: - log('found') - break - elif lengw==0: - log('no match') - assert False - assert len(gmwords) > 0 - log(FG.GREEN + 'GM Patch: ' + FG.WHITE + gmwords[0]) - p = GM_LOWER.index(gmwords[0]) - # for i in range(len(GM_LOWER)): - # continue_search = False - # for pword in inst.split(' '): - # if pword.lower() not in gmwords: - # continue_search = True - # break - # p = i - # stop_search=True - - # if stop_search: - # break - # if continue_search: - # assert i < len(GM_LOWER)-1 - # continue - - self.patch_num = p - # log('PATCH SET - ' + str(p)) - status = (MIDI_PROGRAM<<4) + self.channels[stackidx] - if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: PROGRAM (%s, %s)' % (status,p)) - self.player.write_short(status,p) - def arp(self, notes, count=0, sustain=False, pattern=[1], reverse=False): - self.arp_enabled = True - if reverse: - notes = notes[::-1] - self.arp_notes = notes - self.arp_cycle_limit = count - self.arp_cycle = count - self.arp_pattern = pattern - self.arp_pattern_idx = 0 - self.arp_idx = 0 # use inversions to move this start point (?) - self.arp_once = False - self.arp_sustain = False - def arp_stop(self): - self.arp_enabled = False - self.release_all() - def arp_next(self): - assert self.arp_enabled - note = self.arp_notes[self.arp_idx] - if self.arp_idx+1 == len(self.arp_notes): # cycle? - self.arp_once = True - if self.arp_cycle_limit: - self.arp_cycle -= 1 - if self.arp_cycle == 0: - self.arp_enabled = False - # increment according to pattern order - self.arp_idx = (self.arp_idx+self.arp_pattern[self.arp_pattern_idx])%len(self.arp_notes) - self.arp_pattern_idx = (self.arp_pattern_idx + 1) % len(self.arp_pattern) - self.arp_delay = (self.arp_note_spacing+1.0) % 1.0 - return (note, self.arp_delay) - def tuplet_next(self): - delay = 0.0 - if self.tuplets: - delay = self.tuplet_offset - self.tuplet_offset = (self.tuplet_offset+self.note_spacing) % 1.0 - self.tuplet_count -= 1 - if not self.tuplet_count: - self.tuplets = False - else: - self.tuplet_stop() - if feq(delay,1.0): - return 0.0 - # log(delay) - return delay - def tuplet_stop(self): - self.tuplets = False - self.tuplet_count = 0 - self.note_spacing = 1.0 - self.tuplet_offset = 0.0 - - diff --git a/test/arp.mid b/test/arp.mid new file mode 100644 index 0000000..c1491f5 Binary files /dev/null and b/test/arp.mid differ diff --git a/test/arp.dc b/test/arp.txbt similarity index 100% rename from test/arp.dc rename to test/arp.txbt diff --git a/test/arp2.txbt b/test/arp2.txbt new file mode 100644 index 0000000..3cce97c --- /dev/null +++ b/test/arp2.txbt @@ -0,0 +1,8 @@ +sus/sus&:2|-1 + + + + + +:| +maj7&: diff --git a/test/auto.txbt b/test/auto.txbt new file mode 100644 index 0000000..fdd82f8 --- /dev/null +++ b/test/auto.txbt @@ -0,0 +1,3 @@ +%r=amsynth,helm + + diff --git a/test/cc.txbt b/test/cc.txbt new file mode 100644 index 0000000..f2cbfc2 --- /dev/null +++ b/test/cc.txbt @@ -0,0 +1,6 @@ +%f=loop c15,-2 +1@bs0:0 +b3 +5 +b3 +:| diff --git a/test/columns.dc b/test/columns.dc deleted file mode 100644 index 5bfcc0c..0000000 --- a/test/columns.dc +++ /dev/null @@ -1,7 +0,0 @@ -Cmaj& Bb, -. -. -. -. -. -. diff --git a/test/files.txbt b/test/files.txbt new file mode 100644 index 0000000..29bfbb6 --- /dev/null +++ b/test/files.txbt @@ -0,0 +1,6 @@ +; test autoloading plugins + +`rack + - amsynth + - helm + diff --git a/test/inversions.dc b/test/inversions.txbt similarity index 67% rename from test/inversions.dc rename to test/inversions.txbt index 8f4ab50..3cd5596 100644 --- a/test/inversions.dc +++ b/test/inversions.txbt @@ -1,13 +1,11 @@ %g.5 -maj7 -"b -"c -"d - maj7 "> ">> ">>> - +maj7 +"< +"<< +"<<< diff --git a/test/markers.dc b/test/markers.dc deleted file mode 100644 index 2d6d2b9..0000000 --- a/test/markers.dc +++ /dev/null @@ -1,5 +0,0 @@ -C -:a -D -@a -E diff --git a/test/markers.txbt b/test/markers.txbt new file mode 100644 index 0000000..4091703 --- /dev/null +++ b/test/markers.txbt @@ -0,0 +1,26 @@ +; marker test +; should play 1 2 1 2 3 4 4 5 6 6 6 7 1' (1') 2' 2' 2' +;:| +;|: +;:|: +;||| +1 +2 +:| +3 +:a*2| +5 +:next|: +1' +:|x: +2' +:x*2| +||| +|a: +4 +|| +|next: +6 +:2| +7 +|| diff --git a/test/modes.dc b/test/modes.txbt similarity index 92% rename from test/modes.dc rename to test/modes.txbt index b8b2005..df6d436 100644 --- a/test/modes.dc +++ b/test/modes.txbt @@ -14,7 +14,7 @@ 5 6 7 -%r2 +%k2 1 2 3 diff --git a/test/new.txbt b/test/new.txbt new file mode 100644 index 0000000..c8a3c4b --- /dev/null +++ b/test/new.txbt @@ -0,0 +1,27 @@ +%t=120x4 p=bass,drums c=20,-2 + +1& +? +? +! +? +! +? +? +! +? +! +? +m& +? +? +! +? +! +? +? +! +? +! +? + diff --git a/test/octave.dc b/test/octave.txbt similarity index 100% rename from test/octave.dc rename to test/octave.txbt diff --git a/test/organ.dc b/test/organ.dc deleted file mode 100644 index 94cf28a..0000000 --- a/test/organ.dc +++ /dev/null @@ -1,38 +0,0 @@ -%t90 -%g4 -%ppiano,piano,drums - -a: -maj9/1/1 . 1 - - b5 - - 3 - - b5 - 1) - 1 - - b5 - - 3 - - b5 - 1) -4maj9/4/4 . 1 - - b5 - - 3 - - b5 - 1) - 1 - - b5 - - 3 - - b5 - 1) - 1 diff --git a/test/piano.dc b/test/piano.txbt similarity index 100% rename from test/piano.dc rename to test/piano.txbt diff --git a/test/run.txbt b/test/run.txbt new file mode 100644 index 0000000..43135b5 --- /dev/null +++ b/test/run.txbt @@ -0,0 +1,26 @@ +%t120x2 p=piano,drums,bass c20,-2 + +dim$__ 1 b3,1 +">> +">>>> 1 5 +">> +" +"<< +"<<<< + +4dim$__ 1 +">>> +">>>>>> 1 +">>> +" +"<<< +"<<<<< + +5dim$__ 1 +">> +">>>> 1 +">> +" +"<< +"<<<< + diff --git a/test/run2.txbt b/test/run2.txbt new file mode 100644 index 0000000..8913c0f --- /dev/null +++ b/test/run2.txbt @@ -0,0 +1,57 @@ +; thirds +:3$ +2:b3$ +3:b3$ +4:3$ +5:3$ +6:b3$ +7:b3$ +1:3$' +7:b3$ +6:b3$ +5:3$ +4:3$ +3:b3$ +2:b3$ +:3$ +maj/1/1 + + +; fourths +:4$ +2:4$ +3:4$ +4:#4$ +5:4$ +6:4$ +7:4$ +1:4$' +7:4$ +6:4$ +5:4$ +4:#4$ +3:4$ +2:4$ +:4$ +sus/1/1 + + +; fifths +:5$ +2:5$ +3:5$ +4:5$ +5:5$ +6:5$ +7:b5$ +1:5$' +7:b5$ +6:5$ +5:5$ +4:5$ +3:5$ +2:5$ +:5$ +sus/1/1 + + diff --git a/test/scale.dc b/test/scale.txbt similarity index 100% rename from test/scale.dc rename to test/scale.txbt diff --git a/test/softpiano.dc b/test/softpiano.dc deleted file mode 100644 index 5e88ace..0000000 --- a/test/softpiano.dc +++ /dev/null @@ -1,20 +0,0 @@ -%g.5 - -; maj7, 40% vel, sustain -maj7!4_ -; maj7, 1st/B inversion, 40% vel, sustain -maj7b!4_, - -maj7!7 -maj7b!4_, - -6mb,2_ - -/7m_-_ - -6mb/6_!4-_ - -7m/6_!4-_ - -%g4 - diff --git a/test/softpiano.txbt b/test/softpiano.txbt new file mode 100644 index 0000000..47440ea --- /dev/null +++ b/test/softpiano.txbt @@ -0,0 +1,20 @@ +%g.5 + +; maj7, 40% vel, sustain +maj7!4_ +; maj7, 1st inversion, 40% vel, sustain +maj7>!4_, + +maj7!7 +maj7>!4_, + +6m>,2_ + +/7m_-_ + +6m>/6_!4-- + +7m/6_!4-- + +%g4 + diff --git a/test/strum.dc b/test/strum.txbt similarity index 80% rename from test/strum.dc rename to test/strum.txbt index 902b313..d3b4f69 100644 --- a/test/strum.dc +++ b/test/strum.txbt @@ -1,4 +1,4 @@ -%p=drums,piano +%p=drums,piano,piano %g4 ,2 1 Cmaj$ @@ -17,12 +17,12 @@ 1 iio7... iio7... - 4,3 + 4,,, 1 iio7... iio7... - 4,3 + 4,,, %g1 - 1,2 Cm + 1,2 . . . diff --git a/test/sus.dc b/test/sus.dc deleted file mode 100644 index a0e9b8c..0000000 --- a/test/sus.dc +++ /dev/null @@ -1,19 +0,0 @@ -1 1<1 1<2 -4 -5 -1 -4 -5 -1 1 1 -4 -5 -1 -4 -5 -1 1 1 -4 -5 -1 -4 -5 -1' diff --git a/test/sync.dc b/test/sync.txbt similarity index 90% rename from test/sync.dc rename to test/sync.txbt index 8fd07c6..6ae4e67 100644 --- a/test/sync.dc +++ b/test/sync.txbt @@ -1,5 +1,5 @@ %t=120 g=4 -6<2 3 6<3 +6,2 3 6,3 6 - 6 b3 6 - diff --git a/test/tabs.txbt b/test/tabs.txbt new file mode 100644 index 0000000..7b2247c --- /dev/null +++ b/test/tabs.txbt @@ -0,0 +1,28 @@ +; tab syntax +; not yet impl +||| +%c20,-2 + +|0 | +| 0 | +| 0 | +| 0 | +| 0 | +| 0| + +|0 | +| 1 | +| 2 | +| 3 | +| 4 | +| 5| + +|1 | +| 2 | +| - 3 | +| - 4 | +| - 5 | +| 6 | + +- + diff --git a/test/tempo.dc b/test/tempo.txbt similarity index 100% rename from test/tempo.dc rename to test/tempo.txbt diff --git a/test/channels.dc b/test/tracks.txbt similarity index 100% rename from test/channels.dc rename to test/tracks.txbt diff --git a/test/tuplet.dc b/test/tuplet.txbt similarity index 100% rename from test/tuplet.dc rename to test/tuplet.txbt diff --git a/test/tuplet2.txbt b/test/tuplet2.txbt new file mode 100644 index 0000000..f1050f2 --- /dev/null +++ b/test/tuplet2.txbt @@ -0,0 +1,32 @@ +; quintuplets 5(:8) and 5:6 +%pdrums,drums c12,-2 + +2 3T5!! +2 3T +2 3T +2 3T +2 3T +2 +2 +2 +2 3T5!! +2 3T +2 3T +2 3T +2 3T +2 +2 +2 +2 3T5:6!! +2 3T +2 3T +2 3T +2 3T +2 +2 3T5:6!! +2 3T +2 3T +2 3T +2 3T +2 +2 3! diff --git a/test/walk.txbt b/test/walk.txbt new file mode 100644 index 0000000..a95c902 --- /dev/null +++ b/test/walk.txbt @@ -0,0 +1,19 @@ +1 1,1 1,2 +3 +5 +1 +3 +5 +maj& 1 1 + + + + + +1 1 1 +3 +5 +1 +3 +5 +1' diff --git a/textbeat/__main__.py b/textbeat/__main__.py new file mode 100755 index 0000000..a2f5525 --- /dev/null +++ b/textbeat/__main__.py @@ -0,0 +1,314 @@ +#!/usr/bin/python +"""textbeat +Copyright (c) 2018 Grady O'Connell +Open-source under MIT License + +Examples: + textbeat shell + textbeat -T start tutorial + textbeat song.txbt play song + +Usage: + textbeat [--dev=] [--midi=] [--ring] [--follow] [--stdin] [--vi] [-adeftnpsrxvL] [INPUT] + textbeat [+RANGE] [--dev=] [--midi=] [--ring] [--follow] [--stdin] [--vi] [-adeftnpsrxL] [INPUT] + textbeat [-rhT] + textbeat -c [COMMANDS ...] + textbeat -l [LINE_CONTENT ...] + +Options: + -h --help show this + -v --verbose verbose + -T --tutorial (STUB) tutorial + -t --tempo= (STUB) set tempo [default: 120] + -x --grid= (STUB) set grid [default: 4] + -n --note= (STUB) set grid using note value [default: 1] + -s --speed= (STUB) playback speed [speed: 1.0] + --dev= output device, partial match + -p --patch= (STUB) default midi patch, partial match + -f --flags comma-separated global flags + -c execute commands sequentially + -l execute commands simultaneously + --stdin read entire file from stdin + -r --remote (STUB) realtime remote (control through stdin/out) + --ring don't mute midi on end + -L --loop loop song + --midi= generate midi file + + play from line or maker, for range use start:end + -e --edit (STUB) open file in editor + --vi (STUB) shell vi mode + -H --transpose transpose (in half steps) + --sustain start with sustain enabled + --numbers use note numbers in output + --notenames use note names in output + --flats prefer flats in output (default) + --sharps prefer sharps in output + --lint (STUB) analyze file + --follow tracks file output for editors by printing newlines every line + --quiet no output + -a --analyze (STUB) midi input chord analyzer +""" +from __future__ import absolute_import, unicode_literals, print_function, generators +# try: +from .defs import * +# except: +# from .defs import * +def main(): +# if __name__!='__main__': +# sys.exit(0) + # ARGS = docopt(__doc__.replace('textbeat',os.path.basename(sys.argv[0]).lower())) + ARGS = docopt(__doc__) + set_args(ARGS) + + from . import support + # from .support import * + # from .support import * + + # style = style_from_dict({ + # Token: '#ff0066', + # Token.Prompt: '#00aa00', + # Token.Info: '#000088', + # }) + colorama.init(autoreset=True) + +# logging.basicConfig(filename=LOG_FN,level=logging.DEBUG) + + player = Player() + +# class Marker: +# def __init__(self,name,row): +# self.name = name +# self.line = row + + midifn = None + + for arg,val in iteritems(ARGS): + if val: + if arg == '--tempo': player.tempo = float(val) + elif arg == '--midi': + midifn = val + player.midifile = mido.MidiFile() + player.cansleep = False + elif arg == '--grid': player.grid = float(val) + elif arg == '--note': player.grid = float(val)/4.0 + elif arg == '--speed': player.speed = float(val) + elif arg == '--verbose': player.showtext = True + elif arg == '--dev': + player.portname = val + elif arg == '--vi': player.vimode = True + elif arg == '--patch': + vals = val.split(',') + for i in range(len(vals)): + val = vals[i] + if val.isdigit(): + player.tracks[i].patch(int(val)) + else: + player.tracks[i].patch(val) + elif arg == '--sustain': player.sustain=True + elif arg == '--ring': player.ring=True + elif arg == '--remote': player.remote = True + elif arg == '--lint': LINT = True + elif arg == '--quiet': set_print(False) + elif arg == '--follow': + set_print(False) + player.canfollow = True + elif arg == '--flats': FLATS = True + elif arg == '--sharps': SHARPS= True + elif arg == '--edit': pass + elif arg == '-l': player.cmdmode = 'l' + elif arg == '-c': player.cmdmode = 'c' + elif arg == '-T': player.tutorial = Tutorial(player) + elif arg =='--flags': + vals = val.split(',') + player.add_flags(map(player.FLAGS.index, vals)) + elif arg == '--loop': player.add_flags(Player.Flag.LOOP) + # elif arg == '--renderman': player.renderman = True + + if player.cmdmode=='l': + player.buf = ' '.join(ARGS['LINE_CONTENT']).split(';') # ; + elif player.cmdmode=='c': + player.buf = ' '.join(ARGS['COMMANDS']).split(' ') # spaces + elif not player.tutorial: # mode n + # if len(sys.argv)>=2: + # FN = sys.argv[-1] + FN = ARGS['INPUT'] + from_stdin = False + if FN=='-' or ARGS['--stdin']: + FN = 0 # TEMP: doesn't work with py2 + from_stdin = True + else: + from_stdin = False + if FN or from_stdin: + # player.markers[''] = 0 # start marker + with open(FN) as f: + lc = 0 + for line in f.readlines(): + if line: + if line[-1] == '\n': + line = line[:-1] + elif len(line)>=2 and line[-2:0] == '\r\n': + line = line[:-2] + + # if not line: + # lc += 1 + # continue + ls = line.strip() + + # place marker + if ls.startswith(':'): + bm = ls[1:] + # only store INITIAL marker positions + if not bm in player.markers: + player.markers[bm] = lc + elif ls.startswith('|') and ls.endswith(':'): + bm = ls[1:-1] + # only store INITIAL marker positions + if not bm in player.markers: + player.markers[bm] = lc + + lc += 1 + player.buf += [line] + # player.rowno.append(lc) + player.shell = False + else: + if player.cmdmode == 'n': + player.cmdmode = '' + player.shell = True + + player.interactive = player.shell or player.remote or player.tutorial + + pygame.midi.init() + if pygame.midi.get_count()==0: + error('No midi devices found.') + sys.exit(1) + dev = -1 + +# if player.showtext: +# for i in range(pygame.midi.get_count()): +# log(pygame.midi.get_device_info(i)) + + DEVS = get_defs()['dev'] + if player.showtext: + log('MIDI Devices:') + portnames = [] + breakall = False + firstpass = True + for name in DEVS: + for i in range(pygame.midi.get_count()): + port = pygame.midi.get_device_info(i) + portname = port[1].decode('utf-8') + if port[3]!=1: + continue + if player.showtext: + log(' '*4 + portname) + if player.portname: + if player.portname.lower() in portname.lower(): + player.portname = portname + dev = i + breakall = True + break + else: + if portname.lower().startswith(name): + player.portname = portname + dev = i + breakall = True + break + if firstpass: + portnames += [portname] + + # if port[3]==1: + # continue + firstpass = False + if breakall: + break + +# for i in range(pygame.midi.get_count()): +# port = pygame.midi.get_device_info(i) +# # if port[3]==1: +# # continue +# portname = port[1].decode('utf-8') +# if player.showtext: +# log(' '*4 + portname) +# if player.portname: +# if player.portname.lower() in portname.lower(): +# player.portname = portname +# dev = i +# break +# else: +# for name in DEVS: +# if portname.lower().startswith(name): +# player.portname = portname +# dev = i +# break +# portnames += [portname] + if player.showtext: + log('') + + if dev == -1: + dev = pygame.midi.get_default_output_id() + + player.midi += [pygame.midi.Output(dev)] + player.instrument = 0 + player.midi[0].set_instrument(0) + mch = 0 + for i in range(NUM_CHANNELS_PER_DEVICE): + # log("%s -> %s" % (i,mch)) + player.tracks.append(Track(player, i, mch)) + mch += 2 if i==DRUM_CHANNEL else 1 + + if player.sustain: + player.tracks[0].sustain = player.sustain + +# show nice output in certain modes + if player.shell or player.cmdmode in 'cl': + player.showtext = True + + player.init() + + if player.shell: + log(FG.BLUE + 'textbeat')# v'+str(VERSION)) + log('Copyright (c) 2018 Grady O\'Connell') + log('https://github.com/flipcoder/textbeat') + active = support.SUPPORT_ALL & support.SUPPORT + inactive = support.SUPPORT_ALL - support.SUPPORT + if active: + log(FG.GREEN + 'Active Modules: ' + STYLE.RESET_ALL + ', '.join(active) + STYLE.RESET_ALL) + if inactive: + log(FG.RED + 'Inactive Modules: ' + STYLE.RESET_ALL + ', '.join(inactive)) + if player.portname: + log(FG.GREEN + 'Device: ' + STYLE.RESET_ALL + '%s' % (player.portname if player.portname else 'Unknown',)) + log(FG.RED + 'Other Devices: ' + STYLE.RESET_ALL + '%s' % (', '.join(portnames))) + if player.portname: + if player.tracks[0].midich == DRUM_CHANNEL: + log(FG.GREEN + 'GM Percussion') + else: + log(FG.GREEN + 'GM Patch: '+ STYLE.RESET_ALL +'%s' % GM[player.tracks[0].patch_num]) + + # log('') + # log(FG.BLUE + 'New? Type help and press enter to start the tutorial.') + log('') + + player.run() + + if player.midifile: + player.midifile.save(midifn) + + # TODO: turn all midi note off + i = 0 + for ch in player.tracks: + if not player.ring: + ch.panic() + ch.midi = None + + for mididev in player.midi: + del mididev + player.midi = [] + pygame.midi.quit() + +# if __name__=='__main__': +# curses.wrapper(main) + + support.support_stop() + +if __name__=='__main__': + main() + diff --git a/textbeat/analyzer.py b/textbeat/analyzer.py new file mode 100644 index 0000000..f934de0 --- /dev/null +++ b/textbeat/analyzer.py @@ -0,0 +1 @@ +# import * from diff --git a/textbeat/def/cc.yaml b/textbeat/def/cc.yaml new file mode 100644 index 0000000..32d6ffe --- /dev/null +++ b/textbeat/def/cc.yaml @@ -0,0 +1,38 @@ +cc: + '`': 1 # mod + at: 2 # aftertouch + bc: 2 # breath controller + fc: 4 # foot controller + pt: 5 # portamento time + v: 7 # volume + bl: 8 # balance + pn: 10 # pan + ex: 11 # expression + ga: 16 # general purpose + gb: 17 # " + gc: 18 # " + gd: 19 # " + sp: 64 # sustain pedal + ps: 65 # portamento switch + st: 66 # sostenuto pedal + sf: 67 # soft pedal + lg: 68 # legato pedal + hd: 69 # hold w/ release fade + o: 70 # osc + R: 71 # res + r: 72 # release + a: 73 # attack + f: 74 # filter + sa: 75 # sound ctrl + sb: 76 # " + sc: 77 # " + sd: 78 # " + se: 79 # " + pa: 84 # portmento amount + rv: 91 # reverb + tr: 92 # tremolo + cr: 93 # chorus + ph: 94 # phaser + mo: 126 # mono + po: 127 # poly + diff --git a/textbeat/def/dc.yaml b/textbeat/def/dc.yaml new file mode 100644 index 0000000..8f5f9e0 --- /dev/null +++ b/textbeat/def/dc.yaml @@ -0,0 +1,35 @@ +chords: + # experimental voicings, just for fun -- MAY BE REMOVED + # warnings should be emitted to beginners using these in shell, + # so they don't get confused when learning + + # majadd4 + wu: '3 4 5' + wu7: '3 4 5 7' + wu7b5: '2 3 b5 7' + wu-: 'b3 4 5' + wu-7: 'b3 4 5 b7' + wu-7b5: 'b3 4 b5 b7' + wwu-7: 'b3 4 5 7' + wwu-7b5: 'b3 4 b5 7' + + # edges of scale shape along circle (i.e. darkest and brightest notes of each whole tone scale) + eg: '3 4 7' # diatonic edges ("eg>>"=phyr "eg>3"=lyd "eg>6"=loc) + eg-: '2 b3 7' # melodic minor edges + arp: '3 4 5 7' # aka wu7, diatonic edge w/ 5, for inversions use '|5' eg: eg>|5 + +#shorthand: +# wu: +# replace: ma +# add: '4' +# 'wu-': +# replace: m +# add: '4' +# 'wu+': +# replace: + +# add: '4' + +chord_alts: + sq: sus24 + melo: mmu7 # melodic minor edges w/ 5 + diff --git a/config/def.yaml b/textbeat/def/default.yaml similarity index 79% rename from config/def.yaml rename to textbeat/def/default.yaml index 93412f0..79757f4 100644 --- a/config/def.yaml +++ b/textbeat/def/default.yaml @@ -10,11 +10,11 @@ scales: - aeolian - locrian chromatic: - intervals: '111111111111' + intervals: '11111111111' wholetone: human: 'whole tone' intervals: '222222' - bebop: + bebopmajor: intervals: '2212p121' pentatonic: intervals: '23223' @@ -22,19 +22,27 @@ scales: - yo - minorpentatonic - majorpentatonic - - egyption + - egyptian - mangong blues: intervals: '32p132' melodicminor: intervals: '2122221' + modes: + - melodicminor + - assyrian + - lydianaug + - acoustic + - melodicmajor + - halfdim + - altered harmonicminor: human: 'harmonic minor' intervals: '2122132' modes: - harmonicminor - locriann6 - - ionianaug#5 + - ionianaug - dorian#4 - phyrigianminor - lydian#9 @@ -43,6 +51,7 @@ scales: human: 'harmonic major' intervals: '2122132' modes: + - harmonicmajor - dorianb5 - phyrgianb4 - lydianb3 @@ -58,12 +67,12 @@ scales: - ultraphyrigian - hungarianminor - oriental - - ionianaug + - ionian#2#5 - locrianbb3bb7 neapolitan: intervals: '1222221' modes: - - neapolitan + - neapolitanmajor - leadingwholetone - lydianaugdom - minorlydian @@ -72,7 +81,7 @@ scales: - superlocrianbb3 neapolitanminor: human: 'neapolitan minor' - intervals: '222222' + intervals: '1222131' modes: - neapolitanminor - lydian#6 @@ -84,25 +93,38 @@ scales: chords: '1': '' - #1: '#1' - m2: 'b2' + #'#1': '#1' + ':#2': '#1' + ':b2': 'b2' + ':2': '2' + ':#2': '#2' + ':b3': 'b3' + ':3': '3' + ':4': '4' + ':#4': '#4' + ':b5': 'b5' + ':5': '5' + ':#5': '#5' + ':b6': 'b6' + #':6': '6' + ':#6': '#6' + ':b7': 'b7' + #':7': '7' + ':8': '8' '2': '2' - #2: '#2' + '#2': '#2' b3: 'b3' - m3: 'b3' '3': '3' '4': '4' - #4: '#4' + '#4': '#4' b5: 'b5' '5': '5' - #5: '#5' + '#5': '#5' b6: 'b6' - #5: '#5' - p6: '6' + '#6': '#6' p7: '7' b7: 'b7' - #6: '#6' - #8: '#8' + '8': '8' #9: '#9' # '#11: '#11' # easy confusion with 11 chord @@ -110,18 +132,17 @@ chords: ma: '3 5' mab5: '3 b5' # lyd - ma#4: '3 #4 5' # lydadd5 + ma#4: '3 #4 5' # lyd5 ma6: '3 5 6' ma69: '3 5 6 9' ma769: '3 5 6 7 9' m69: 'b3 5 6 9' ma7: '3 5 7' - ma7#4: '3 #4 5 7' # lyd7add5 + ma7#4: '3 #4 5 7' # lyd75 ma7b5: '3 b5 7' # lyd7 ma7b9: '3 5 7 b9' ma7b13: '3 5 7 9 11 b13' ma9: '3 5 7 9' - # 'maadd9: '3 5 9' ma9b5: '3 b5 7 9' ma9+: '3 #5 7 9' ma#11: '3 b5 7 9 #11' @@ -129,8 +150,6 @@ chords: ma11b5: '3 b5 7 9 11' ma11+: '3 #5 7 9 11' ma11b13: '3 #5 7 9 11 b13' - # 'maadd11: '3 5 11' - # 'maadd#11: '3 5 #11' ma13: '3 5 7 9 11 13' ma13b5: '3 b5 7 9 11 13' ma13+: '3 #5 7 9 11 13' @@ -140,7 +159,6 @@ chords: m7: 'b3 5 b7' m69: 'b3 5 6 9' m769: 'b3 5 6 7 9' - # 'madd9: '3 b5 9' m7b5: 'b3 b5 b7' m7+: 'b3 #5 b7' m9: 'b3 5 b7 9' @@ -169,6 +187,7 @@ chords: 7+: '3 #5 b7' 7b9: '3 5 b7 b9' '9': '3 5 b7 9' + '#9': '3 5 b7 #9' 9b5: '3 b5 b7 9' 9+: '3 #5 b7 9' '9#11': '3 5 b7 9 #11' @@ -188,49 +207,39 @@ chords: dim7: 'b3 b5 bb7' dim9: 'b3 b5 bb7 9' dim11: 'b3 b5 bb7 9 11' + dimma7: 'b3 b5 7' sus: '4 5' sus2: '2 5' - - # majminor mm7: 'b3 5 7' mm9: 'b3 5 7 9' - mm11: 'b3 5 7 9 11' - mm13: 'b3 5 7 9 11 13' - - # power chords - f: '5' pow: '5 8' + # tunings (1 = lowest) + guitar: '4 b7 b10 12 15' # 1=E + bass: '4 b7 b10' # 1=E + bass5: '4 b7 b10 b13' # 1=B + bass6: '4 b7 b10 b13 16' # 1=B + +tune: + guitar: 'E,' + bass: 'E,,' + bass5: 'B,,' + bass6: 'B,,' + chord_alts: r: '1' - M2: '2' - M3: '3' aug: + + aug7: '7+' ma#5: + - ma#5: + - aug7: +7 p4: '4' p5: '5' -: m M: ma sus4: sus - # major: maj ma7: ma7 ma9: ma9 - lyd: mab5 - lyd7: ma7b5 - plyd: ma#4 - plyd7: ma7#4 - # Madd9: maadd9 - # maor7: ma7 - Mb5: mab5 - # M7: ma7 - # M7b5: ma7b5 - # min: m - # minor: m - # min7: m7 - # minor7: m7 - # 11th: 11 + oma7: dimma7 + p: pow o: dim o7: dim7 7o: dim7 @@ -238,10 +247,4 @@ chord_alts: 9o: dim9 o11: dim11 11o: dim11 - sus24: sq - # mma7: mm7 - # mma9: mm9 - # mma11: mm11 - # mma13: mm13 - p: pow - + diff --git a/textbeat/def/dev.yaml b/textbeat/def/dev.yaml new file mode 100644 index 0000000..cb85d88 --- /dev/null +++ b/textbeat/def/dev.yaml @@ -0,0 +1,17 @@ +dev: + - synth input port # linux qsynth + - timidity port 0 + - loopmidi + - loopbe + - fluidsynth-midi + - fluid-synth + - bassmidi driver (port a) + - microsoft midi mapper # lowest preference + - zynaddsubfx + - hexter + - midi in + - virtual raw midi # carla + - midi through + - virmidi + - qjackctl + diff --git a/textbeat/def/exp.yaml b/textbeat/def/exp.yaml new file mode 100644 index 0000000..8f5f9e0 --- /dev/null +++ b/textbeat/def/exp.yaml @@ -0,0 +1,35 @@ +chords: + # experimental voicings, just for fun -- MAY BE REMOVED + # warnings should be emitted to beginners using these in shell, + # so they don't get confused when learning + + # majadd4 + wu: '3 4 5' + wu7: '3 4 5 7' + wu7b5: '2 3 b5 7' + wu-: 'b3 4 5' + wu-7: 'b3 4 5 b7' + wu-7b5: 'b3 4 b5 b7' + wwu-7: 'b3 4 5 7' + wwu-7b5: 'b3 4 b5 7' + + # edges of scale shape along circle (i.e. darkest and brightest notes of each whole tone scale) + eg: '3 4 7' # diatonic edges ("eg>>"=phyr "eg>3"=lyd "eg>6"=loc) + eg-: '2 b3 7' # melodic minor edges + arp: '3 4 5 7' # aka wu7, diatonic edge w/ 5, for inversions use '|5' eg: eg>|5 + +#shorthand: +# wu: +# replace: ma +# add: '4' +# 'wu-': +# replace: m +# add: '4' +# 'wu+': +# replace: + +# add: '4' + +chord_alts: + sq: sus24 + melo: mmu7 # melodic minor edges w/ 5 + diff --git a/textbeat/def/gm.yaml b/textbeat/def/gm.yaml new file mode 100644 index 0000000..7a3ab5c --- /dev/null +++ b/textbeat/def/gm.yaml @@ -0,0 +1,129 @@ +patches: + - Acoustic Grand Piano + - Bright Acoustic Piano + - Electric Grand Piano + - Honky-tonk Piano + - Electric Piano 1 + - Electric Piano 2 + - Harpsichord + - Clavi + - Celesta + - Glockenspiel + - Music Box + - Vibraphone + - Marimba + - Xylophone + - Tubular Bells + - Dulcimer + - Drawbar Organ + - Percussive Organ + - Rock Organ + - Church Organ + - Reed Organ + - Accordion + - Harmonica + - Tango Accordion + - Acoustic Guitar (nylon) + - Acoustic Guitar (steel) + - Electric Guitar (jazz) + - Electric Guitar (clean) + - Electric Guitar (muted) + - Overdriven Guitar + - Distortion Guitar + - Guitar harmonics + - Acoustic Bass + - Electric Bass (finger) + - Electric Bass (pick) + - Fretless Bass + - Slap Bass 1 + - Slap Bass 2 + - Synth Bass 1 + - Synth Bass 2 + - Violin + - Viola + - Cello + - Contrabass + - Tremolo Strings + - Pizzicato Strings + - Orchestral Harp + - Timpani + - String Ensemble 1 + - String Ensemble 2 + - SynthStrings 1 + - SynthStrings 2 + - Choir Aahs + - Voice Oohs + - Synth Voice + - Orchestra Hit + - Trumpet + - Trombone + - Tuba + - Muted Trumpet + - French Horn + - Brass Section + - SynthBrass 1 + - SynthBrass 2 + - Soprano Sax + - Alto Sax + - Tenor Sax + - Baritone Sax + - Oboe + - English Horn + - Bassoon + - Clarinet + - Piccolo + - Flute + - Recorder + - Pan Flute + - Blown Bottle + - Shakuhachi + - Whistle + - Ocarina + - Lead 1 (square) + - Lead 2 (sawtooth) + - Lead 3 (calliope) + - Lead 4 (chiff) + - Lead 5 (charang) + - Lead 6 (voice) + - Lead 7 (fifths) + - Lead 8 (bass + lead) + - Pad 1 (new age) + - Pad 2 (warm) + - Pad 3 (polysynth) + - Pad 4 (choir) + - Pad 5 (bowed) + - Pad 6 (metallic) + - Pad 7 (halo) + - Pad 8 (sweep) + - FX 1 (rain) + - FX 2 (soundtrack) + - FX 3 (crystal) + - FX 4 (atmosphere) + - FX 5 (brightness) + - FX 6 (goblins) + - FX 7 (echoes) + - FX 8 (sci-fi) + - Sitar + - Banjo + - Shamisen + - Koto + - Kalimba + - Bag pipe + - Fiddle + - Shanai + - Tinkle Bell + - Agogo + - Steel Drums + - Woodblock + - Taiko Drum + - Melodic Tom + - Synth Drum + - Reverse Cymbal + - Guitar Fret Noise + - Breath Noise + - Seashore + - Bird Tweet + - Telephone Ring + - Helicopter + - Applause + - Gunshot diff --git a/textbeat/def/informal.yaml b/textbeat/def/informal.yaml new file mode 100644 index 0000000..b2eb58d --- /dev/null +++ b/textbeat/def/informal.yaml @@ -0,0 +1,35 @@ +chords: + + # add2 + mu: '2 3 5' + mu7: '2 3 5 7' + mu7#4: '2 3 #4 5 7' # lyd7 3sus2|sus2 + mu7b5: '2 3 b5 7' + 'mu-': '2 b3 5' + 'mu-7': '2 b3 5 b7' + 'mu-7b5': '2 3 b5 b7' + 'mmu7': '2 b3 5 7' + 'mmu7#4': '2 b3 #4 5 7' + 'mmu7b5': '2 b3 b5 7' + + # jazz sus voicings + q: '4 b7' # quartal (stacked 4ths) sus voicing + qt: '5 9' # quintal (stacked 5ths) sus voicing + sus7: '4 5 b7' + sus9: '4 5 b7 9' + sus24: '2 4 5' + +#shorthand: +# mu: +# replace: ma +# add: '2' + +chord_alts: + lyd: mab5 + lyd7: ma7b5 + plyd: ma#4 + lyd5: ma#4 + lyd57: ma7#4 + plyd7: ma7#4 + lyd75: ma7#4 + diff --git a/textbeat/def/style.yaml b/textbeat/def/style.yaml new file mode 100644 index 0000000..66ae429 --- /dev/null +++ b/textbeat/def/style.yaml @@ -0,0 +1,2 @@ +# playstyle variables like vibrato speed and depth +style: diff --git a/src/__init__.py b/textbeat/defs.py similarity index 51% rename from src/__init__.py rename to textbeat/defs.py index 4767e63..4b2e934 100644 --- a/src/__init__.py +++ b/textbeat/defs.py @@ -1,24 +1,30 @@ #!/usr/bin/python -from __future__ import unicode_literals, print_function, generators +from __future__ import absolute_import, unicode_literals, print_function, generators import os, sys, time, random, itertools, signal, tempfile, traceback, socket +import time, subprocess, pipes, collections +from collections import OrderedDict from builtins import range, str, input from future.utils import iteritems -import time, subprocess, pipes import yaml, colorama, appdirs from docopt import docopt -from collections import OrderedDict -import pygame, pygame.midi as midi +import mido +with open(os.devnull, 'w') as devnull: + # suppress pygame messages + stdout = sys.stdout + sys.stdout = devnull + import pygame, pygame.midi + sys.stdout = stdout from multiprocessing import Process,Pipe from prompt_toolkit import prompt -from prompt_toolkit.styles import style_from_dict +# from prompt_toolkit.styles import style_from_dict from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.history import FileHistory -from prompt_toolkit.token import Token +# from prompt_toolkit.token import Token if sys.version_info[0]==3: basestring = str -VERSION = '0.1' +# VERSION = '0.1' FG = colorama.Fore BG = colorama.Back STYLE = colorama.Style @@ -32,11 +38,12 @@ RANGE = 109 OCTAVE_BASE = 5 DRUM_WORDS = ['drum','drums','drumset','drumkit','percussion'] -SPEECH_WORDS = ['speech','say','speak'] -CCHAR = ' <>=~.\'`,_&|!?*\"$(){}[]%' -CCHAR_START = 'T' # control chars +CCHAR = ' <>=~.\'`,_&|!?*\"$(){}[]%@;' +CCHAR_START = 'TV' # control chars PRINT = True +def bit(x): + return 1 << x def cmp(a,b): return bool(a>b) - bool(a line + self.returns = {} # repeat row -> number of rpts left + # self.returns[row] = 0 + +class Player(object): + + class Flag: + ROMAN = bit(0) + TRANSPOSE = bit(1) + LOOP = bit(2) + FLAGS = [ + 'roman', + 'transpose', + 'loop' + ] + # FLAGS = set([ + # 'roman', # STUB: fit roman chord in scale shape + # 'transpose', # allow transposition of note letters + # 'loop' + # ]) + + def __init__(self): + self.quitflag = False + self.vimode = False + self.bcproc = None + self.log = False + self.canfollow = False + self.cansleep = True + self.lint = False + self.tracks_active = 1 + self.showmidi = False + self.scale = DIATONIC + self.mode = 1 + self.transpose = 0 + self.octave = 0 + self.tempo = 90.0 + self.grid = 4.0 # Grid subdivisions of a beat (4 = sixteenth note) + self.columns = 0 + self.column_shift = 0 + self.showtextstr = [] + self.showtext = False # nice output (-v), only shell and cmd modes by default + self.sustain = False # start sustained + self.ring = False # disables midi muting on program exit + self.buf = [] + self.markers = {} + f = StackFrame(-1,-1,0) + f.returns[''] = 0 + self.tutorial = None + self.callstack = [f] + self.separators = [] + self.track_history = ['.'] * NUM_TRACKS + self.fn = None + self.row = 0 + # self.rowno = [] + self.startrow = -1 + self.stoprow = -1 + self.cmdmode = 'n' # n normal c command s sequence + self.schedule = Schedule(self) + self.host = [] + self.tracks = [] + self.shell = False + self.remote = False + self.interactive = False + self.gui = False + self.portname = '' + self.speed = 1.0 + self.muted = False # mute all except for solo tracks + self.midi = [] + self.instrument = None + self.t = 0.0 # actual time + self.last_follow = 0 + self.last_marker = -1 + self.midifile = None + self.flags = 0 + self.version = '0' + self.auto = False + self.embedded_files = {} + + # require enable at top of file + self.devices = ['midi'] + + def init(self): + + for i in range(len(sys.argv)): + arg = sys.argv[i] + + # play range (+ param, comma-separated start and end) + if arg.startswith('+'): + vals = arg[1:].split(',') + try: + self.startrow = int(vals[0]) + except ValueError: + try: + self.startrow = self.markers[vals[0]] + except KeyError: + log('invalid entry point') + self.quitflag = True + try: + self.stoprow = int(vals[1]) + except ValueError: + try: + # we cannot cut buf now, since seq might be non-linear + self.stoprow = self.markers[vals[0]] + except KeyError: + log('invalid stop point') + self.quitflag = True + except IndexError: + pass # no stop param + + # read embedded files + for row in self.buf: + embedded_fn = None + embedded_file = [] + base_indent = '' + if embedded_fn: + if row.startswith(' ') or row.startswith('\t'): + if not base_indent: + base_indent = row[0] * count_seq(row) + embedded_file = self.row + embedded_file += [row[base_indent:]] + else: + base_indent = '' + self.embedded_files[embedded_fn] = embedded_file + embedded_fn = None + embedded_file = [] + + if row.startswith('`'): + embedded_fn = row[1:] + embedded_file = [] + + # out(self.embedded_files.keys()) + + def refresh_devices(self): + # determine output device support and load external programs + # try: + from .support import supports, SUPPORT_PLUGINS + # except: + # import textbeat.support as support + for dev in self.devices: + if not supports(dev): + if dev!='auto': + out('Device not supported by system: ' + dev) + else: + out('Loading instrument presets requires a compatible host module.') + assert False + try: + # support_enable[dev](self.rack) + SUPPORT_PLUGINS[dev].enable(self.host) + except KeyError: + # no init needed, silent + pass + self.auto = 'auto' in self.devices + + def set_host(self, plugins): + self.host = plugins + self.refresh_devices() + + # def remove_flags(self, f): + # pass + def add_flags(self, f): + if isinstance(f, basestring): + f = 1 << self.FLAGS.index(f) + elif isinstance(f, int): + assert f > 0 + else: + for e in f: + if e.startswith('-'): + # TODO: cancel flag? + # e = e[1:] + # self.flags &= ~e + pass + else: + self.add_flags(e) + return + self.flags |= f + def has_flags(self, f): + if isinstance(f, basestring): + f = 1 << self.FLAGS.index(f) + elif isinstance(f, int): + assert f > 0 + else: + vals = f + f = 0 + i = 0 + for e in self.FLAGS: + if e in vals: + f |= 1 << i + i += 1 + # for e in vals: + # f |= 1 << self.FLAGS.index(e) + return + return self.flags & f + + # for editor integration: "follows" the output of the file by printing + # line numbers to stdout for every parsed line + def follow(self): + if self.startrow==-1 and self.canfollow: + cursor = self.row + 1 + if cursor != self.last_follow: + print(cursor) + self.last_cursor = cursor + # out(self.rowno[self.row]) + + def pause(self): + try: + for ch in self.tracks[:self.tracks_active]: + ch.release_all(True) + out('') + input('PAUSED: Press ENTER to resume. Press Ctrl-C To quit.') + except: + return False + return True + + def write_midi_tempo(self): + # set initial midifile tempo + if self.midifile: + if not self.midifile.tracks: + self.midifile.tracks.append(mido.MidiTrack()) + self.midifile.tracks[0].append(mido.MetaMessage( + 'set_tempo', tempo=mido.bpm2tempo(self.tempo) + )) + + def run(self): + for ch in self.tracks: + ch.refresh() + + self.header = True + embedded_file = False + + self.write_midi_tempo() + + while not self.quitflag: + self.follow() + + try: + self.line = '.' + try: + self.line = self.buf[self.row] + if self.row == self.startrow: + self.startrow = -1 + if self.stoprow!=-1 and self.row == self.stoprow: + self.buf = [] + raise IndexError + except IndexError: + if self.has_flags(Player.Flag.LOOP): + self.row = 0 + continue + + self.row = len(self.buf) + # done with file, finish playing some stuff + + arps_remaining = 0 + if self.interactive or self.cmdmode in ['c','l']: # finish arps in shell mode + for ch in self.tracks[:self.tracks_active]: + if ch.arp_enabled: + if ch.arp_cycle_limit or not ch.arp_once: + arps_remaining += 1 + self.line = '.' + if not arps_remaining and not self.shell and self.cmdmode not in ['c','l']: + break + self.line = '.' + + if not arps_remaining and not self.schedule.pending(): + if self.interactive: + for ch in self.tracks[:self.tracks_active]: + ch.release_all() + + if self.shell: + # self.shell PROMPT + # log(orr(self.tracks[0].scale,self.scale).mode_name(orr(self.tracks[0].mode,self.mode))) + # cur_oct = self.tracks[0].octave + # cline = FG.GREEN + 'txbt> '+FG.BLUE+ '('+str(int(self.tempo))+'bpm x'+str(int(self.grid))+' '+\ + # note_name(self.tracks[0].transpose) + ' ' +\ + # orr(self.tracks[0].scale,self.scale).mode_name(orr(self.tracks[0].mode,self.mode,-1))+\ + # ')> ' + modename = orr(self.tracks[0].scale,self.scale).mode_name(orr(self.tracks[0].mode,self.mode,-1)) + + keynote = note_name(self.transpose + self.tracks[0].transpose) + keynote = keynote if keynote!='C' else '' + parts = [ + str(int(self.tempo))+'bpm', # tempo + 'x'+str(int(self.grid)), # subdiv + keynote, + ('' if modename=='ionian' else modename) + ] + cline = 'txbt> ('+ \ + ' '.join(filter(lambda x: x, parts))+ \ + ')> ' + # if bufline.endswith('.txbt'): + # play file? + # bufline = raw_input(cline) + bufline = prompt(cline, history=HISTORY, vi_mode=self.vimode) + bufline = list(filter(None, bufline.split(' '))) + bufline = list(map(lambda b: b.replace(';',' '), bufline)) + elif self.remote: + pass + else: + assert False + + self.buf += bufline + + continue + + else: + break + + log(FG.MAGENTA + self.line) + + # cells = line.split(' '*2) + + # if line.startswith('|'): + # self.separators = [] # clear + # # column setup! + # for i in range(1,len(line)): + # if line[i]=='|': + # self.separators.append(i) + + # log(BG.RED + line) + + # skip reading embedded files, since they're already in mem + if self.line.startswith('`'): + embedded_file = True + self.row += 1 + continue + elif self.line.startswith(' ') or self.line.startswith('\t'): + if embedded_file: + self.row += 1 + continue + else: + embedded_file = False + + fullline = self.line[:] + self.line = self.line.strip() + + # LINE COMMANDS + ctrl = False + cells = [] + + if self.line: + # COMMENTS (;) + if self.line[0] == ';' and not self.line.startswith(';;'): + self.row += 1 + continue + + # set marker + # if self.line[-1]==':': # suffix marker + # # allow override of markers in case of reuse + # self.markers[self.line[:-1]] = self.row + # self.callstack[-1].returns[self.row] = 0 + # self.row += 1 + # continue + # # continue + if self.line[0]=='#' and self.line[-1]=='#': + # track title, ignore + self.row += 1 + continue + + # TODO: global 'silent' commands (doesn't consume time) + if self.line.startswith('%'): + self.line = self.line[1:].strip() # remove % and spaces + for tok in self.line.split(' '): + if not tok: + break + if tok[0]==' ': + tok = tok[1:] + var = tok[0].upper() + if var in 'TGXNPSRCKFDR': # global vars % + cmd = tok.split(' ')[0] + op = cmd[1] + try: + val = cmd[2:] + except: + val = '' + # log("op val %s %s" % (op,val)) + if op == ':': op = '=' + if not op in '*/=-+': + # implicit = + val = str(op) + str(val) + op='=' + if not val or op=='.': + val = op + val # append + # TODO: add numbers after dots like other ops + if val[0]=='.': + note_value(val) + ct = count_seq(val) + val = pow(0.5,count) + op = '/' + num,ct = peel_uint(val[:ct]) + elif val[0]=='*': + op = '*' + val = pow(2.0,count_seq(val)) + if op=='/': + if var in 'GX': self.grid/=float(val) + elif var=='N': self.grid/=float(val) #! + elif var=='T': self.tempo/=float(val) + else: assert False + elif op=='*': + if var in 'GX': self.grid*=float(val) + elif var=='N': self.grid*=float(val) #! + elif var=='T': self.tempo*=float(val) + else: assert False + elif op=='+': + if var=='K': self.transpose += note_offset('#1' if val=='+' else val) + # elif var=='O': self.octave += int(1 if val=='+' else val) + elif var=='T': self.tempo += max(0,float(val)) + elif var in 'GX': self.grid += max(0,float(val)) + else: assert False + # if var=='K': + # self.octave += -1*sgn(self.transpose)*(self.transpose//12) + # self.transpose = self.transpose%12 + elif op=='-': + if var=='K': + self.transpose -= note_offset(val) + out(note_offset(val)) + # elif var=='O': self.octave -= int(1 if val=='-' else val) + elif var=='T': self.tempo -= max(0,float(val)) + elif var in 'GX': self.grid -= max(0,float(val)) + else: assert False + # self.octave += -1*sgn(self.transpose)*(self.transpose//12) + # if var=='K': + # self.octave += -1*sgn(self.transpose)*(self.transpose//12) + # self.transpose = self.transpose%12 + elif op=='=': + if var in 'GX': self.grid=float(val) + elif var=='R': + if not 'auto' in self.devices: + self.devices = ['auto'] + self.devices + self.set_host(val.split(',')) + elif var=='V': self.version = val + elif var=='D': + self.devices = val.split(',') + self.refresh_devices() + # elif var=='O': self.octave = int(val) + elif var=='N': self.grid=float(val)/4.0 #! + elif var=='T': + vals = val.split('x') + self.tempo=float(vals[0]) + try: + self.grid = float(vals[1]) + except: + pass + elif var=='C': + vals = val.split(',') + self.columns = int(vals[0]) + try: + self.column_shift = int(vals[1]) + except: + pass + elif var=='P': + vals = val.split(',') + for i in range(len(vals)): + p = vals[i] + if p.strip().isdigit(): + self.tracks[i].patch(int(p)) + else: + self.tracks[i].patch(p) + elif var=='F': # flags + self.add_flags(val.split(',')) + # for i in range(len(vals)): # TODO: ? + # self.tracks[i].add_flags(val.split(',')) + # elif var=='O': + # self.octave = int(val) + elif var=='K': + self.transpose = note_offset(val) + # self.octave += -1*sgn(self.transpose)*(self.transpose//12) + # self.transpose = self.transpose%12 + elif var=='S': + # var R=relative usage deprecated + try: + if val: + val = val.lower() + # ambiguous alts + + if val.isdigit(): + modescale = (self.scale.name,int(val)) + else: + alts = {'major':'ionian','minor':'aeolian'} + # try: + # modescale = (alts[val[0],val[1]) + # except KeyError: + # pass + val = val.lower().replace(' ','') + + try: + modescale = MODES[val] + except KeyError: + raise NoSuchScale() + + try: + self.scale = SCALES[modescale[0]] + self.mode = modescale[1] + inter = self.scale.intervals + self.transpose = 0 + # log(self.mode-1) + + if var=='R': + for i in range(self.mode-1): + inc = 0 + try: + inc = int(inter[i]) + except ValueError: + pass + self.transpose += inc + elif var=='S': + pass + except ValueError: + raise NoSuchScale() + # else: + # self.transpose = 0 + + except NoSuchScale: + out(FG.RED + 'No such scale.') + pass + else: assert False # no such var + else: assert False # no such op + + if var=='T': + if self.midifile: + if not self.midifile.tracks: + self.midifile.tracks.append(mido.MidiTrack()) + self.midifile.tracks[0].append(mido.MetaMessage( + 'set_tempo', tempo=mido.bpm2tempo(int( + val.split('x')[0] + )) + )) + self.row += 1 + continue + + # set marker here + if (self.line[0]=='|' or self.line.startswith(':|')) and self.line[-1]==':': + # allow override of markers in case of reuse + frame = self.callstack[-1] + if self.line[0]==':': # :|: + bm = self.line[self.line.index('|')+1:-1] + else: + bm = self.line[1:-1] # |: + self.markers[bm] = self.row + frame.markers[bm] = self.row + # self.callstack[-1].returns[self.row] = 0 + self.last_marker = self.row + self.row += 1 + if self.line[0]!=':': # |blah: + # marker only, not repeat + continue + # marker AND repeat, continue to repeat parser section + + if self.line.startswith('|||'): + self.quitflag = True + continue + elif self.line.startswith('||'): + if len(self.callstack)>1: + frame = self.callstack[-1] + frame.count = max(0,frame.count-1) + if frame.count: + self.row = frame.row + 1 + continue + else: + self.row = frame.caller + 1 + self.callstack = self.callstack[:-1] + continue + else: + self.quitflag = True + continue + if self.line[0]==':' and self.line[-1] in '|:' and '|' in self.line: + jumpline = self.line[1:self.line.index('|')] + frame = self.callstack[-1] + jumpline = jumpline.split('*') # *n = n repeats + bm = jumpline[0] + if bm.isdigit(): + bm = '' + count = int(jumpline[0]) + else: + count = int(jumpline[1]) if len(jumpline)>1 else 1 + # frame = self.callstack[-1] + # if count: # repeats remaining + + if bm: + bmrow = self.markers[bm] + else: + bmrow = self.last_marker + + # if not bm: + # frame.count = max(0,frame.count-1) + # if frame.count: + # self.row = frame.row + 1 + # continue + # else: + # self.row += 1 + # continue + + # if bmrow in frame.returns: + + # return to marker (no pushing) + # self.callstack.append(StackFrame(bmrow, self.row, count)) + # self.markers[jumpline[0]] = bmrow + # self.row = bmrow + 1 + # self.last_marker = bmrow + + if bmrow==self.last_marker or bm in frame.markers: # call w/o push? + # ctx already passed bookmark, call w/o pushing (return to mark) + if self.row in frame.returns: # have we repeated yet? + rpt = frame.returns[self.row] + if rpt>0: + frame.returns[self.row] = rpt - 1 + self.row = bmrow + 1 # repeat + else: + del frame.returns[self.row] # reset + self.row += 1 + else: + # start return count + frame.returns[self.row] = count - 1 + self.row = bmrow + 1 # repeat + else: + # mark not yet passed, do push/pop + self.callstack.append(StackFrame(bmrow, self.row, count)) + self.markers[bm] = bmrow + self.row = bmrow + 1 + self.last_marker = bmrow + + # else: + # retcount = frame.returns[self.row] + # if retcount > count: + # self.row = bmrow + 1 + # frame.returns[self.row] -= 1 + # else: + # self.row += 1 + # else: + # self.callstack.append(StackFrame(bmrow, self.row, count)) + # self.markers[jumpline[0]] = bmrow + # self.row = bmrow + 1 + # self.last_marker = bmrow + continue + + # this is not indented in blank lines because even blank lines have this logic + gutter = '' + if self.shell: + cells = list(filter(None,self.line.split(' '))) + elif self.columns: + cells = fullline + # shift column pos right if any + cells = ' ' * max(0,-self.column_shift) + cells + # shift columns right, creating left-hand gutter + # cells = cells[-1*min(0,self.column_shift):] # create gutter (if negative shift) + # separate into chunks based on column width + cells = [cells[i:i + self.columns] for i in range(0, len(cells), self.columns)] + # log(cells) + elif not self.separators: + # AUTOGENERATE CELL self.separators + cells = fullline.split(' ') + pos = 0 + for cell in cells: + if cell: + if pos: + self.separators.append(pos) + # log(cell) + pos += len(cell) + 1 + # log( "self.separators " + str(self.separators)) + cells = list(filter(None,cells)) + # if fullline.startswith(' '): + # cells = ['.'] + cells # dont filter first one + autoseparate = True + else: + # SPLIT BASED ON self.separators + s = 0 + seplen = len(self.separators) + # log(seplen) + pos = 0 + for i in range(seplen): + cells.append(fullline[pos:self.separators[i]].strip()) + pos = self.separators[i] + lastcell = fullline[pos:].strip() + if lastcell: cells.append(lastcell) + + # make sure active tracks get empty cell + len_cells = len(cells) + if len_cells > self.tracks_active: + self.tracks_active = len_cells + else: + # add empty cells for active tracks to the right + cells += ['.'] * (self.tracks_active - len_cells) + del len_cells + + cell_idx = 0 + + # CELL LOGIC + # cells += ['.'] * (tracks_active - len(cells)) + for cell in cells: + + cell = cells[cell_idx] + ch = self.tracks[cell_idx] + fullcell = cell[:] + ignore = False + skipcell = False + + # if self.instrument != ch.instrument: + # self.player.set_instrument(ch.instrument) + # self.instrument = ch.instrument + + cell = cell.strip() + if cell: + self.header = False # contents here, no longer in file header + + if cell.count('\"') == 1: # " is recall, but multiple " means lyrics/speak? + cell = cell.replace("\"", self.track_history[cell_idx]) + else: + self.track_history[cell_idx] = cell + + fullcell_sub = cell[:] + + # empty + # if not cell: + # cell_idx += 1 + # continue + + if cell and cell[0]=='-': + if self.shell: + ch.stop() + else: + ch.release_all() # don't mute sustain + cell_idx += 1 + continue + if cell=='--': + ch.sustain = False + cell_idx += 1 + continue + + if cell and cell[0]=='=': # hard stop + ch.panic() + cell_idx += 1 + continue + if cell=='==': + ch.panic() + ch.sustain = False + cell_idx += 1 + continue + + if cell and cell[0]=='-': # stop prefix + ch.release_all(True) + # ch.sustain = False + cell = cell[1:] + + notecount = len(ch.scale.intervals if ch.scale else self.scale.intervals) + # octave = int(cell[0]) // notecount + c = cell[0] if cell else '' + + # PROCESS NOTE + chord_notes = [] # notes to process from chord + notes = [] # outgoing notes to midi + slashnotes = [[]] # using slashchords, to join this into notes [] above + allnotes = [] # same, but includes all scheduled notes + accidentals = False + # loop = True + noteloop = True + expanded = False # inside chord? if so, don't advance cell itr + events = [] + inversion = 1 # chord inversion + flip_inversion = False + inverted = 0 # notes pending inversion + chord_root = 1 + chord_note_count = 0 # include root + chord_note_index = -1 + octave = self.octave + ch.octave + strum = 0.0 + noteletter = '' # track this just in case (can include I and V) + chordname = '' + chordnames = [] + + # frets = bool(ch.frets) + # frets = False + # if cell and len(cell.strip())>1 and cell[0]=='|' and cell[-1]!=':': + # cells = cells.lstrip()[1:] + # frets = True + + cell_before_slash=cell[:] + sz_before_slash=len(cell) + slash = cell.split('/') # slash chords + # log(slash) + tok = slash[0] + cell = slash[0][:] + slashidx = 0 + addbottom = False # add note at bottom instead + # slash = cell[0:min(cell.find(n) for n in '/|')] + + # chordnameslist = [] + # chordnoteslist = [] + # chordrootslist = [] + + while True: + n = 1 + roman = 0 # -1 lower, 0 none, 1 upper, + accidentals = '' + # number_notes = False + + # if not chord_notes: # processing cell note + # pass + # else: # looping notes of a chord? + + if tok and not tok=='.': + + # sharps/flats before note number/name + c = tok[0] + if c=='b' or c=='#': + if len(tok) > 2 and tok[0:2] =='bb': + accidentals = 'bb' + n -= 2 + tok = tok[2:] + if not expanded: cell = cell[2:] + elif c =='b': + accidentals = 'b' + n -= 1 + tok = tok[1:] + if not expanded: cell = cell[1:] + elif len(tok) > 2 and tok[0:2] =='##': + accidentals = '##' + n += 2 + tok = tok[2:] + if not expanded: cell = cell[2:] + elif c =='#': + accidentals = '#' + n += 1 + tok = tok[1:] + if not expanded: cell = cell[1:] + + # try to get roman numeral or number + c,ct = peel_roman_s(tok) + ambiguous = 0 + # Help parser ambiguities (TODO: make these automatic) + # (These are names that begin with letters or roman numerals) + for amb in ('ion','dor','dim','dom','alt','dou','egy','aeo','dia','gui','bas','aug'): + ambiguous += tok.lower().startswith(amb) + if ct and not ambiguous: + lower = (c.lower()==c) + c = ['','i','ii','iii','iv','v','vi','vii','viii','ix','x','xi','xii'].index(c.lower()) + noteletter = note_name(c-1,NOTENAMES,FLATS) + roman = -1 if lower else 1 + else: + # use normal numbered note + num,ct = peel_int(tok) + c = num + + # couldn't get it, set c back to char + if not ct: + c = tok[0] if tok else '' + + if c=='.': + tok = tok[1:] + cell = cell[1:] + + # tok2l = tok.lower() + # if tok2l in SOLEGE_NOTES or tok2l.startswith('sol'): + # # SOLFEGE_NOTES = + # pass + + # note numbers, roman, numerals or solege + lt = len(tok) + if ct: + c = int(c) + if c == 0: + ignore = True + # TODO: allow zero if only if fretting mode + # skipcell = True + cell = cell[1:] + break + # n = 1 + # break + # numbered notation + # wrap notes into 1-7 range before scale lookup + wrap = ((c-1) // notecount) + note = ((c-1) % notecount)+1 + # log('note ' + str(note)) + + for i in range(1,note): + # dont use scale for expanded chord notes + if expanded: + try: + n += int(DIATONIC.intervals[i-1]) + except ValueError: + n += 1 # passing tone + else: + m = orr(ch.mode,self.mode,-1)-1 + steps = orr(ch.scale,self.scale).intervals + idx = steps[(i-1 + m) % notecount] + n += int(idx) + if inverted: # inverted counter + if flip_inversion: + # log((chord_note_count-1)-inverted) + inverted -= 1 + else: + # log('inversion?') + # log(n) + n += 12 + inverted -= 1 + assert inversion != 0 + if inversion!=1: + if flip_inversion: # not working yet + # log('note ' + str(note)) + # log('down inv: %s' % (inversion//chord_note_count+1)) + # n -= 12 * (inversion//chord_note_count+1) + pass + else: + # log('inv: %s' % (inversion//chord_note_count)) + n += 12 * (inversion//chord_note_count) + # log('---') + # log(n) + # log('n slash %s,%s' %(n,slashidx)) + n += 12 * (wrap - slashidx) + + # log(tok) + tok = tok[ct:] + if not expanded: cell = cell[ct:] + + # number_notes = not roman + + # if tok and tok[0]==':': # currently broken? wrong notes + # n += 1 + # tok = tok[1:] # allow chord sep + # if not expanded: cell = cell[1:] + + # log('note: %s' % n) + + # NOTE LETTERS + elif c.upper() in '#ABCDEFG' and not ambiguous: + + n = 0 + # flats, sharps after note names? + # if tok: + if lt >= 3 and tok[1:3] =='bb': + accidentals = 'bb' + n -= 2 + tok = tok[0] + tok[3:] + cell = cell[0:1] + cell[3:] + elif lt >= 2 and tok[1] == 'b': + accidentals = 'b' + n -= 1 + tok = tok[0] + tok[2:] + if not expanded: cell = cell[0] + cell[2:] + elif lt >= 3 and tok[1:3] =='##': + accidentals = '##' + n += 2 + tok = tok[0] + tok[3:] + cell = cell[0:1] + cell[3:] + elif lt >= 2 and tok[1] =='#': + accidentals = '#' + n += 1 + tok = tok[0] + tok[2:] + if not expanded: cell = cell[0] + cell[2:] + # accidentals = True # dont need this + + if not tok: + c = 'b' # b note was falsely interpreted as flat + + # note names, don't use these in chord defn + try: + # dont allow lower case, since 'b' means flat + note = ' CDEFGAB'.index(c.upper()) + noteletter = str(c) + for i in range(note): + n += int(DIATONIC.intervals[i-1]) + n -= slashidx*12 + # adjust B(7) and A(6) below C, based on accidentials + nn = (n-1)%12+1 # n normalized + if (8<=nn<=9 and accidentals.startswith('b')): # Ab or Abb + n -= 12 + elif nn == 10 and not accidentals: + n -= 12 + elif nn > 10: + n -= 12 + tok = tok[1:] + + if ch.flags & Player.Flag.TRANSPOSE: + # compensate so note letters are absolute + n -= self.transpose + ch.transpose + + if not expanded: cell = cell[1:] + except ValueError: + ignore = True + else: + ignore = True # re-enable if there's a chord listed + + # CHORDS + addnotes = [] + is_chord = False + if not expanded: + if tok or roman: + # log(tok) + cut = 0 + nonotes = [] + chordname = '' + reverse = False + # addhigherroot = False + + # cut chord name from text after it + for char in tok: + if cut==0 and char in CCHAR_START: + break + if cut!=1 and char.isupper(): + break + if char in CCHAR: + break + if char == '\\': + reverse = True + break + # if char == '^': + # addhigherroot = True + # break + chordname += char + try: + # TODO: addchords + + # TODO omit note (Maj7no5) + if chordname[-2:]=='no': + # print(tok) + # print(cut) + cut += 1 # The 'o' of no + numberpart = tok[cut:] + # print(numberpart) + # second check will throws + int_numberpart = None + try: + int_numberpart = int(numberpart) + except ValueError: + pass + if numberpart[0] in '#b' or int_numberpart!=None: + # if tok[] + prefix,ct = peel_any(tok[cut:],'#b') + # print('prefix', prefix) + # print('ct', ct) + if ct: cut += ct + + num,ct = peel_uint(tok[cut:]) + if ct: + cut += ct + # cut += 1 # remove "no" + chordname = chordname[:-2] # cut "no" + if prefix: + nonotes.append(str(prefix)+str(num)) # ex: b5 + else: + nonotes.append(str(num)) # ex: b5 + # print(nonotes) + break + + # cut += 2 + + except IndexError: + log('bad chordname index') + pass # chordname length + except ValueError: + log('bad cast ' + char) + pass # bad int(char) + cut += 1 + # i += 1 + # else: + # try: + # if tok[cut+1]==AMBIGUOUS_CHORDS[chordname]: + # continue # read ahead to disambiguate + # except: + # break + + # try: + # # number chords w/o note letters aren't chords + # if int(chordname) and not noteletter: + # chordname = '' # reject + # except: + # pass + + # log(chordname) + # don't include tuplet in chordname + if 'add' in chordname: + # out(chordname) + addtoks = chordname.split('add') + # out(addtoks) + chordname = addtoks[0] + addnotes = addtoks[1:] + + if chordname.endswith('T'): + chordname = chordname[:-1] + cut -= 1 + + # log(chordname) + if roman: # roman chordnames are sometimes empty + if chordname: #and not chordname[1:] in 'bcdef': + if roman == -1: # minor + if chordname[0] in '6719': + chordname = 'm' + chordname + else: + chordname = 'maj' if roman>0 else 'm' + chordname + + if chordname: + # log(chordname) + if chordname in BAD_CHORDS: + # certain chords may parse wrong w/ note letters + # example: aug, in this case, 'ug' is the bad chord name + chordname = noteletter + chordname # fix it + n -= 1 # fix chord letter + + # letter inversions deprecated (use <>) + # try: + # inv_letter = ' abcdef'.index(chordname[-1]) + + # # num,ct = peel_int(tok[cut+1:]) + # # if ct and num!=0: + # # cut += ct + 1 + # if inv_letter>=1: + # inversion = max(1,inv_letter) + # inverted = max(0,inversion-1) # keep count of pending notes to invert + # # cut+=1 + # chordname = chordname[:-1] + + # except ValueError: + # pass + + try: + chord_notes = expand_chord(chordname) + chord_notes = list(filter(lambda x: x not in nonotes, chord_notes)) + chord_note_count = len(chord_notes)+1 # + 1 for root + expanded = True + tok = "" + cell = cell[cut:] + is_chord = True + except KeyError as e: + # may have grabbed a ctrl char, pop one + if len(chord_notes)>1: # can pop? + try: + chord_notes = expand_chord(chordname[:-1]) + chord_notes = list(filter(lambda x,nonotes=nonotes: x in nonotes)) + chord_note_count = len(chord_notes) # + 1 for root + expanded = True + try: + tok = tok[cut-1:] + cell = cell[cut-1:] + is_chord = True + except: + assert False + except KeyError: + log('key error') + break + else: + # noteloop = True + # assert False + # invalid chord + log(FG.RED + 'Invalid Chord: ' + chordname) + break + + if is_chord: + # assert not accidentals # accidentals with no note name? + if reverse: + if '1' in nonotes: + chord_notes = chord_notes[::-1] + else: + chord_notes = chord_notes[::-1] + ['1'] + else: + if '1' not in nonotes: + chord_notes = ['1'] + chord_notes + + chord_notes += addnotes # TODO: sort + # slashnotes[0].append(n + chord_root - 1 - slashidx*12) + # chordnameslist.append(chordname) + # chordnoteslist.append(chord_notes) + # chordrootslist.append(chord_root) + chord_root = n + ignore = False # re-enable default root if chord was w/o note name + continue + else: + pass + # assert False # not a chord, treat as note + # break + else: # blank chord name + # log('blank chord name') + # expanded = False + pass + else: # not tok and not expanded + # log('not tok and not expanded') + pass + # else and not chord_notes: + # # last note in chord, we're done + # tok = "" + # noteloop = False + + slashnotes[0].append(n + chord_root-1) + + + if expanded: + if not chord_notes: + # next chord + expanded = False + + if chord_notes: + tok = chord_notes[0] + chord_notes = chord_notes[1:] + chord_note_index += 1 + # fix negative inversions + if inversion < 0: # not yet working + # octave += inversion/chord_note_count + inversion = inversion%chord_note_count + inverted = -inverted + flip_inversion = True + + if not expanded: + inversion = 1 # chord inversion + flip_inversion = False + inverted = 0 # notes pending inversion + chord_root = 1 + chord_note_count = 0 # include root + chord_note_index = -1 + chord_note_index = -1 + # next slash chord part + flip_inversion = False + inversion = 1 + chord_notes = [] + slash = slash[1:] + if slash: + tok = slash[0] + cell = slash[0] + slashnotes = [[]] + slashnotes + else: + break + slashidx += 1 + # if expanded and not chord_notes: + # break + + notes = [i for o in slashnotes for i in o] # combine slashnotes + cell = cell_before_slash[sz_before_slash-len(cell):] + + # if frets: + # ch.strings = notes + # notes = [] + + # 'ignore' means do outer break + if ignore: + allnotes = [] + notes = [] + # if skipcell: # 0 or something weird, completely skip line + # break + + # save the intended notes since since scheduling may drop some + # during control phase + allnotes = notes + sustain = ch.sustain + delay = 0.0 + + # TODO: arp doesn't work if channel not visible/present, move this + if ch.arp_enabled: + if notes: # incoming notes? + # log(notes) + # interrupt arp + ch.arp_stop() + else: + # continue arp + if ch.arp_next(self.shell or self.cmdmode in 'lc'): + notes = [ch.arp_note] + delay = ch.arp_delay + sustain = ch.arp_sustain + if not fzero(delay): + ignore = False + # schedule=True + + # if notes: + # log(notes) + + cell = cell.strip() # ignore spaces + + vel = ch.vel + stop = False + showtext = [] + arpnotes = False + arpreverse = False + arppattern = [1] + duration = 0.0 + + # if cell and cell[0]=='|': + # if not expanded: cell = cell[1:] + + # log(cell) + + # ESPEAK / FESTIVAL support wip + # if cell.startswith('\"') and cell.count('\"')==2: + # quote = cell.find('\"',1) + # word = cell[1:quote] + # BGPIPE.send((BGCMD.SAY,str(word))) + # cell = cell[quote+1:] + # ignore = True + + # cell = self.fx(cell) + + accent = '' + notevalue = '' + tuplets = False + while len(cell) >= 1: # recompute len before check + if fullcell=='.': + break + spacer = False + if cell.strip() and cell[0] in '@ ': + spacer = True + cell = cell[count_seq(cell):] + + after = [] # after events + clen = len(cell) + # All tokens here must be listed in CCHAR + + ## + and - symbols are changed to mean minor and aug chords + # if c == '+': + # log("+") + # c = cell[1] + # shift = int(c) if c.isdigit() else 0 + # mn = n + base + (octave+shift) * 12 + c = cell[0] + c2 = None + if clen: + c2 = cell[:2] + + if c: c = c.lower() + if c2: c2 = c2.lower() + + # if c == '-' or c == '+' or c.isdigit(c): + # cell = cell[1:] # deprecated, ignore + # continue + + # OCTAVE SHIFT UP + # if sym== '>': ch.octave = octave # persist + # row_events += 1 + # elif c == '-': + # c = cell[1] + # shift = int(c) if c.isdigit() else 0 + # p = base + (octave+shift) * 12 + ct = 0 + # CELL COMMENTS + if c2==';;': # cell comment + cell = [] + break + #INVERSIONS + elif c == '>' or c=='<': + sign = (1 if c=='>' else -1) + ct = count_seq(cell) + for i in range(ct): + if notes: + notes[i%len(notes)] += 12*sign + notes = notes[sign*1:] + notes[:1*sign] + # when used w/o note/chord, track history should update + # self.track_history[cell_idx] = fullcell_sub + # log(notes) + if ch.arp_enabled: + ch.arp_notes = ch.arp_notes[1:] + ch.arp_notes[:1] + cell = cell[ct:] + # OCTAVE SHIFTING + elif c == ',' or c=='\'': + cell = cell[1:] + sign = 1 if c=='\'' else -1 + if cell and cell[0].isdigit(): # numbers persist + shift,ct = peel_int(cell,1) + cell = cell[ct:] + octave += sign*shift + ch.octave = octave # persist + else: + rpt = count_seq(cell,',') + octave += sign*(rpt+1) # persist + cell = cell[rpt:] + # SET OCTAVE + elif c == '=': + cell = cell[1:] + if cell and cell[0].isdigit(): + octave = int(cell[0]) + cell = cell[1:] + else: + octave = 0 # default + shift = 1 + ch.octave = octave + # row_events += 1 + # PITCH WHEEL + elif clen>1 and c=='~': + cell = cell[1:] + # sn = 1.0 + if cell[0]=='/' or cell[0]=='\\': + # sn = 1.0 if cell[0]=='/' else -1.0 + cell = cell[1:] + num,ct = peel_uint_s(cell) + if ct: + onum = num + num = float('0.'+num) + num *= 1.0 if c=='/' else -1.0 + sign = 1 + if num<0: + num=onum[1:] + num = float('0.'+num) + sign = -1 + vel = constrain(sign*int(num*127.0),127) + cell = cell[ct:] + else: + if cell and cell[0]=='|': + cell = cell[1:] + vel = min(127,int(ch.vel + 0.5*(127.0-ch.vel))) + ch.pitch(vel) + # VIBRATO + elif c == '~': + ch.mod(127) # TODO: pitch osc in the future + cell = cell[1:] + # MOD WHEEL + # elif c == '`': # mod wheel -- moved to CC + # ch.mod(127) + # cell = cell[1:] + # MUTE SUSTAIN + elif cell.startswith('--'): + num, ct = count_seq('-') + sustain = ch.sustain = False + cell = cell[ct:] + # STOP/PANIC + elif cell.startswith('='): + num, ct = count_seq('=') + if num==2: + ch.stop() + elif num==3: + ch.panic() + if num<=3: + sustain = ch.sustain = False + cell = cell[ct:] + # SUSTAIN HOLD + elif c2=='__': + sustain = ch.sustain = True + ch.arp_sustain = True + cell = cell[2:] + # [DEPRECATED] STOP SUSTAIN + elif c2=='_-': + sustain = False + cell = cell[2:] + # SUSTAIN + elif c=='_': + ch.arp_sustain = True + sustain = True + cell = cell[1:] + # elif c=='v': # volume - moved to CC + # cell = cell[1:] + # # get number + # num = '' + # for char in cell: + # if char.isdigit(): + # num += char + # else: + # break + # assert num != '' + # cell = cell[len(num):] + # vel = int((float(num) / float('9'*len(num)))*127) + # ch.cc(7,vel) + # RECORD SEQ + elif cell.startswith('^^'): + cell = cell[2:] + r,ct = peel_uint(cell,0) + ch.record(r) + cell = cell[ct:] + # REPLAY SEQ + elif cell.startswith('^'): + cell = cell[1:] + r,ct = peel_uint(cell,0) + if self.showtext: + showtext.append('play sequence: ' + num) + ch.replay(r) + cell = cell[ct:] + # MIDI CHANNEl + elif c2=='ch': + num,ct = peel_uint(cell[1:]) + cell = cell[1+ct:] + if self.showtext: + showtext.append('midi channel: ' + num) + ch.midi_channel(num) + # SCALE + elif c=='s': + # solo if used by itself (?) + # scale if given args + # ch.soloed = True + cell = cell[1:] + # MUTE + elif c=='m': + ch.enable(c=='m') + cell = cell[1:] + # MIDI CC + elif c=='c': # MIDI CC + # get number + cell = cell[1:] + cc,ct = peel_int(cell) + assert ct + cell = cell[ct+1:] + ccval,ct = peel_int(cell) + assert ct + if not ct: + error('cc requires value') + cell = [] + cell = cell[ct:] + ccval = int(num) + ch.cc(cc,ccval) + # PROGRAM/PATCH CHANGE + elif c=='p': + # bank select as other args? + cell = cell[1:] + p,ct = peel_uint(cell) + if not ct: + error('p command requires number') + cell = [] + cell = cell[ct:] + ch.patch(p) + # BANK SELECT + elif c2=='bs': + cell = cell[2:] + num,ct = peel_uint(cell) + cell = cell[ct:] + b = num + if cell and cell[0]==':': + cell = cell[1:] + num2,ct = peel_uint(cell) + if not ct: + error(': params require number') + cell = [] + cell = cell[ct:] + b = num2 # second val -> lsb + b |= num << 8 # first value -> msb + ch.bank(b) + # NOTE LENGTH + elif c=='*': + dots = count_seq(cell) + if notes: + notevalue = '*' * dots + cell = cell[dots:] + num,ct = peel_uint_s(cell) + if ct: + num = float('0.'+num) + cell = cell[ct:] + else: + num = 1.0 + if dots==1: + duration = num + events.append(Event(num, lambda _: ch.release_all(), ch)) + else: + duration = num*pow(2.0,float(dots-1)) + events.append(Event(num*pow(2.0,float(dots-1)), lambda _: ch.release_all(), ch)) + else: + cell = cell[dots:] + if self.showtext: + showtext.append('duration(*)') + # PLACEHOLDER + elif c=='.': + dots = count_seq(cell) + if len(c)>1 and notes: + cell = cell[dots:] + if ch.arp_enabled: + dots -= 1 + notevalue = '.' * dots + if dots: + num,ct = peel_uint_s(cell) + if ct: + num = int('0.' + num) + cell = cell[ct:] + else: + num = 1.0 + duration = num*pow(0.5,float(dots)) + events.append(Event(num*pow(0.5,float(dots)), lambda _: ch.release_all(), ch)) + else: + cell = cell[dots:] + if self.showtext: + showtext.append('shorten(.)') + # NOTE TIME SHIFT + elif c=='(' or c==')': # note shift (early/delay) + num = '' + cell = cell[1:] + s,ct = peel_uint(cell, 5) + if ct: + cell = cell[ct:] + delay = -1*(c=='(')*float('0.'+num) if num else 0.5 + if delay < 0.0: + error('delay >= 0') + cell = [] + continue + # MARKER (ignore -- already parsed) + elif c=='|': + cell = [] + # MARKER (ignore -- already parsed) + elif c==':': + cell = [] + # FULL ACCENT + elif c2=='!!': + accent = '!!' + cell = cell[2:] + vel,ct = peel_uint_s(cell,127) + if ct: + cell = cell[ct:] + ch.vel = vel # persist if numbered + else: + if ch.max_vel >= 0: + vel = ch.max_vel + else: + vel = 127 + if self.showtext: + showtext.append('accent(!!)') + # ACCENT + elif c=='!': + accent = '!' + cell = cell[1:] + # num,ct = peel_uint_s(cell) + # if ct: + # vel = constrain(int(float('0.'+num)*127.0),127) + # cell = cell[ct:] + # else: + if ch.accent_vel >= 0: + vel = ch.accent_vel + else: + vel = constrain(int(ch.vel + 0.5*(127.0-ch.vel)),127) + if self.showtext: + showtext.append('accent(!)') + # GHOST NOTE + elif c2=='??': + accent = '??' + if ch.ghost_vel >= 0: + vel = ch.ghost_vel + else: + vel = max(0,int(ch.vel*0.25)) + cell = cell[2:] + if self.showtext: + showtext.append('soften(??)') + # SOFT NOTE + elif c=='?': + accent = '?' + if ch.soft_vel >= 0: + vel = ch.soft_vel + else: + vel = max(0,int(ch.vel*0.5)) + cell = cell[1:] + if self.showtext: + showtext.append('soften(?)') + # elif cell.startswith('$$') or (c=='$' and lennotes==1): + # STRUM/SPREAD/TREMOLO + elif c=='$': + sq = count_seq(cell) + cell = cell[sq:] + num,ct = peel_uint_s(cell,'0') + if ct: + cell = cell[ct:] + num = float('0.'+num) + strum = 1.0 + if len(notes)==1: # tremolo + notes = notes * 2 + # notes = [notes[i:i + sq] for i in range(0, len(notes), sq)] + # log('strum') + if spacer: + ch.soft_vel = vel + if self.showtext: + showtext.append('strum($)') + # ARPEGGIO + elif c=='&': + count = count_seq(cell) + arpcount,ct = peel_uint(cell[count:],0) + # notes = list(itertools.chain.from_iterable(itertools.repeat(\ + # x, count) for x in notes\ + # )) + cell = cell[ct+count:] + if count==2: arpreverse = True + if not notes: + # & restarts arp (if no note) + ch.arp_restart() + # ch.arp_sustain = sustain + else: + arpnotes = True + arppattern = [] + while True: + if not (not arppattern and cell.startswith(':')) or\ + (arppattern and cell.startswith('|')): + break + out(cell[1:]) + num,ct = peel_int(cell[1:],1) + if not ct: + break + arppattern += [num] + cell = cell[1+ct:] + if self.showtext: + showtext.append('arpeggio(&)') + # TUPLETS + elif c=='t': + tuplets = True + tups = count_seq(cell,'t') + Tups = count_seq(cell[tups:],'T') + cell = cell[tups+Tups:] + if not ch.tuplets: + ch.tuplets = True + pow2i = 0.0 + num,ct = peel_uint(cell,'3') + cell = cell[ct:] + ct2=0 + denom = 0 + if cell and cell[0]==':': + denom,ct2 = peel_float(cell[1:]) + cell = cell[1+ct2:] + if not ct2: + for i in itertools.count(): + denom = 1 << i + if denom > num: + break + # out('denom' + str(denom)) + # out('num ' + str(num)) + ch.note_spacing = denom/float(num) # ! + ch.tuplet_count = int(num) + ch.tuplet_offset = 0.0 + # elif c==':': + # if not notes: + # cell = [] + # continue # ignore marker + # GLOBAL VARS (ignore -- already parsed) + elif c=='%': + # ctrl line + cell = [] + break + # MIDI CC + elif c2 in CC: + cell = cell[2:] + num,ct = peel_uint_s(cell) + if ct: + num = float('0.'+num) + cell = cell[ct:] + else: + if cell and cell[0]=='!': + cell = cell[1:] + num = 1.0 + ch.cc(CC[c2],constrain(int(num*127.0),127)) + # PERSISTENT VELOCITY + elif c in '0123456789': + # set persistent track velocity for accent level + num,ct = peel_uint(cell) + cell = cell[ct:] + if accent=='?': + ch.soft_vel = num + elif accent=='??': + ch.ghost_vel = num + elif accent=='!': + ch.vel = num + elif accent=='!!': + ch.max_vel = num + else: + ch.vel = num + vel = num + # MIDI CC + elif c in CC: + cell = cell[1:] + num,ct = peel_uint_s(cell) + if ct: + num = float('0.'+num) + cell = cell[ct:] + else: + num = 1.0 + ch.cc(CC[c],constrain(int(num*127.0),127)) + # PARSE ERROR + else: + # if self.cmdmode in 'cl': + log(FG.BLUE + self.line) + indent = ' ' * (len(fullcell)-len(cell)) + log(FG.RED + indent + "^ Unexpected " + cell[0] + " here") + cell = [] + ignore = True + break + + # elif c=='/': # bend in + # elif c=='\\': # bend down + + base = (OCTAVE_BASE+octave) * 12 - 1 + self.transpose + ch.transpose + p = base + + if arpnotes: + ch.arp(notes, arpcount, sustain, arppattern, arpreverse, octave) + arpnext = ch.arp_next(self.shell or self.cmdmode in 'lc') + notes = [ch.arp_note - (octave*12)] # [HACK] first note already has frame octave offset above + delay = ch.arp_delay + # if not fcmp(delay): + # pass + # schedule=True + + if notes and not tuplets and not sustain: + ch.release_all() + + for ev in events: + self.schedule.add(ev) + events = [] + + delta = 0 # how much to separate notes + if strum < -EPSILON: + notes = notes[::-1] # reverse + strum -= strum + if strum > EPSILON: + ln = len(notes) + delta = (1.0/(ln*forr(duration,1.0))) # t between notes + + if self.showtext: + # log(FG.MAGENTA + ', '.join(map(lambda n: note_name(p+n), notes))) + # chordoutput = chordname + # if chordoutput and noletter: + # coordoutput = note_name(chord_root+base) + chordoutput + # log(FG.CYAN + chordoutput + " ("+ \) + # (', '.join(map(lambda n,base=base: note_name(base+n),notes)))+")" + # log(showtext) + showtext = [] + if chordname and not ignore: + noteletter = note_name(n+base) + # for cn in chordnames: + # log(FG.CYAN + noteletter + cn + " ("+ \) + # (', '.join(map(lambda n,base=base: note_name(base+n),allnotes)))+")" + + if not tuplets: + ch.tuplet_stop(); + delay += ch.tuplet_next() + + i = 0 + for n in notes: + # if no schedule, play note immediately + # also if scheduled, play first note of strum if there's no delay + if fzero(delay): + # if not schedule or (i==0 and strum>=EPSILON and delay\n'+\ + '\n'+\ + ''+ext+'\n'+\ + ''+name+'\n'+\ + 'x'+\ + '\n'+\ + '\n'+\ + 'N\n'+\ + 'Yes\n'+\ + ''+hex(i)+'\n'+\ + ''+\ + '\n\n' + i += 1 + filebuf = filebuf.replace('', ''+instrumentxml) + with open(self.temp_proj,'w') as f: + f.write(filebuf) + + self.proj = self.temp_proj + self.gen_inited = True + else: + self.proj = fn.split('.')[0]+'.carxp' + if os.path.exists(proj): + log(proj) + self.proc = subprocess.Popen(['carla',proj], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # '--nogui', + elif not rack: + log('To load a Carla project headless, create a \'%s\' file.' % proj) + + self.initialized = True + def supported(self): + return not ERROR + def support(self): + return ['auto','carla'] + def stop(self): + if self.gen_inited and self.proj: + try: + os.remove(carla_proj[1]) + except OSError: + pass + except FileNotFoundError: + pass + + if self.self.temp_proj: + os.unlink(self.temp_proj) + if self.self.proc: + self.proc.kill() + +# instrument.export(FluidSynth) +export = Carla + diff --git a/textbeat/plugins/csound.py b/textbeat/plugins/csound.py new file mode 100755 index 0000000..c5c40b1 --- /dev/null +++ b/textbeat/plugins/csound.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +import textbeat.instrument as instrument +from textbeat.instrument import Instrument +from shutilwhich import which +import subprocess + +ERROR = False +if not which('csound'): + ERROR = True + +class CSound(Instrument): + NAME = 'csound' + def __init__(self, args): + Instrument.__init__(self, CSound.NAME) + self.initialized = False + self.proc = None + self.csound = None + def enable(self): + if not initialized: + self.proc = subprocess.Popen(['csound', '-odac', '--port='+str(CSOUND_PORT)], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + self.csound = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.initialized = True + def enabled(self): + return self.initialized + def supported(self): + return not ERROR + def support(self): + return ['csound'] + def send(self, s): + assert self.initialized + return csound.sendto(s,('localhost',CSOUND_PORT)) + # def note_on(self, t, n, v): + # self.fs.noteon(t, n, v) + # def note_off(self, t, n, v): + # self.fs.noteoff(t, v) + # pass + def stop(self): + self.proc.kill() + pass + +export = CSound + diff --git a/src/support.py b/textbeat/plugins/espeak.py old mode 100644 new mode 100755 similarity index 50% rename from src/support.py rename to textbeat/plugins/espeak.py index 60eb0c5..05cbd0a --- a/src/support.py +++ b/textbeat/plugins/espeak.py @@ -1,25 +1,17 @@ -from . import get_args -ARGS = get_args() -SUPPORT = set(['midi']) -SUPPORT_ALL = set(['sonic-pi','csound','midi']) # gme,mpe -psonic = None -if ARGS['--sonic-pi']: - import psonic - SUPPORT.add('sonic-pi') +#!/usr/bin/env python +from textbeat.defs import * +import textbeat.instrument as instrument +from textbeat.instrument import Instrument +from shutilwhich import which +import subprocess -csound = None -if ARGS['--csound']: - csound_proc = subprocess.Popen(['csound', '-odac', '--port='+str(CSOUND_PORT)], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - csound = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - SUPPORT.add('csound') - -def csound_send(s): - assert csound - return csound.sendto(s,('localhost',CSOUND_PORT)) +ERROR = False +if not which('espeak'): + ERROR = True # Currently not used, caches text to speech stuff in a way compatible with jack # current super slow, need to write stabilizer first -class BackgroundProcess: +class BackgroundProcess(object): def __init__(self, con): self.con = con self.words = {} @@ -50,7 +42,7 @@ def run(self): self.words.clear() else: log('BAD COMMAND: ' + msg[0]) - self.processses = list(filter(lambda p: p.poll()==None, self.processes)) + self.processes = list(filter(lambda p: p.poll()==None, self.processes)) self.con.close() for tmp in self.words: tmp.close() @@ -58,18 +50,41 @@ def run(self): proc.wait() def bgproc_run(con): - proc = BackgroundProcess(con) - proc.run() + self.proc = BackgroundProcess(con) + self.proc.run() + +class ESpeak(Instrument): + NAME = 'espeak' + def __init__(self, args): + Instrument.__init__(self, ESpeak.NAME) + self.initialized = False + self.proc = None + self.espeak = None + def enable(self): + if not initialized: + self.pipe, child = Pipe() + self.proc = Process(target=bgproc_run, args=(child,)) + self.proc.start() + + self.initialized = True + def enabled(self): + return self.initialized + def supported(self): + return not ERROR + def support(self): + return ['espeak'] + # def note_on(self, t, n, v): + # self.fs.noteon(t, n, v) + # def note_off(self, t, n, v): + # self.fs.noteoff(t, v) + # pass + def stop(self): + if self.proc: + self.pipe.send((BGCMD.QUIT,)) + self.proc.join() -BGPROC = None -# BGPIPE, child = Pipe() -# BGPROC = Process(target=bgproc_run, args=(child,)) -# BGPROC.start() + # self.proc.kill() + pass -def support_stop(): - if csound and csound_proc: - csound_proc.kill() - if BGPROC: - BGPIPE.send((BGCMD.QUIT,)) - BGPROC.join() +export = ESpeak diff --git a/textbeat/plugins/fluidsynth.py b/textbeat/plugins/fluidsynth.py new file mode 100755 index 0000000..e9967e5 --- /dev/null +++ b/textbeat/plugins/fluidsynth.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +import textbeat.instrument as instrument +from textbeat.instrument import Instrument +from shutilwhich import which + +ERROR = False +if which('fluidsynth'): + try: + import fluidsynth # https://github.com/flipcoder/pyfluidsynth + except: + ERROR = True +else: + ERROR = True + +class FluidSynth(Instrument): + NAME = 'fluidsynth' + def __init__(self, args): + Instrument.__init__(self, FluidSynth.NAME) + self.initialized = False + self.enabled = False + self.soundfonts = [] + def init(self): + self.initialized = True + def inited(self): + return self.initialized + def enabled(self): + return self.enabled + def enable(self): + self.fs = fluidsynth.Synth() + self.enabled = True + def soundfont(self, fn, track, bank, preset): + sfid = self.fs.sfload(fn) + self.fs.program_select(track, sfid, bank, preset) + return sfid + def supported(self): + return not ERROR + def support(self): + return ['fluidsynth','soundfonts'] + def note_on(self, t, n, v): + self.fs.noteon(t, n, v) + def note_off(self, t, n, v): + self.fs.noteoff(t, v) + pass + def stop(self): + pass + +# instrument.export(FluidSynth) +export = FluidSynth + diff --git a/textbeat/plugins/sonicpi.py b/textbeat/plugins/sonicpi.py new file mode 100755 index 0000000..f48a8e1 --- /dev/null +++ b/textbeat/plugins/sonicpi.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +import textbeat.instrument as instrument +from textbeat.instrument import Instrument + +ERROR = False +try: + import psonic +except ImportError: + ERROR = True + +class SonicPi(Instrument): + NAME = 'sonicpi' + def __init__(self, args): + Instrument.__init__(self, SonicPi.NAME) + self.initialized = False + def enable(self): + self.initialized = True + def enabled(self): + return self.initialized + def supported(self): + return not ERROR + def support(self): + return ['sonicpi'] + def stop(self): + pass + +# instrument.export(SonicPi) +export = SonicPi + diff --git a/textbeat/plugins/supercollider.py b/textbeat/plugins/supercollider.py new file mode 100755 index 0000000..4a07bfe --- /dev/null +++ b/textbeat/plugins/supercollider.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +import textbeat.instrument as instrument +from textbeat.instrument import Instrument +from shutilwhich import which + +ERROR = False +if which('scsynth'): + try: + import pythonosc + except: + ERROR = True +else: + ERROR = True + +class SuperCollider(Instrument): + NAME = 'supercollider' + def __init__(self, args): + Instrument.__init__(self, SuperCollider.NAME) + self.initialized = False + def enable(self): + self.initialized = True + def enabled(self): + return self.enabled + def supported(self): + return not ERROR + def support(self): + return ['supercollider'] + def stop(self): + pass + +export = SuperCollider + diff --git a/textbeat/presets/default.carxp b/textbeat/presets/default.carxp new file mode 100644 index 0000000..6b74caf --- /dev/null +++ b/textbeat/presets/default.carxp @@ -0,0 +1,37 @@ + + + + + true + false + true + false + 200 + 4000 + + + + + + + Carla:AudioOut1 + AudioOut:playback_1 + + + Carla:AudioOut1 + AudioOut:playback_1 + + + Carla:AudioOut2 + AudioOut:playback_2 + + + Carla:AudioOut2 + AudioOut:playback_2 + + + MidiIn:Virtual Raw MIDI 1-0:VirMIDI 1-0 20:0 + Carla:MidiIn + + + diff --git a/textbeat/presets/example.carxp b/textbeat/presets/example.carxp new file mode 100644 index 0000000..a508f69 --- /dev/null +++ b/textbeat/presets/example.carxp @@ -0,0 +1,63 @@ + + + + + true + false + true + false + 200 + 4000 + + + + + LV2 + amsynth + http://code.google.com/p/amsynth/amsynth + + + + Yes + N + 0x0 + + + + + + LV2 + Helm + http://tytel.org/helm + + + + Yes + N + 0x1 + + + + + + Carla:AudioOut1 + AudioOut:playback_1 + + + Carla:AudioOut1 + AudioOut:playback_1 + + + Carla:AudioOut2 + AudioOut:playback_2 + + + Carla:AudioOut2 + AudioOut:playback_2 + + + MidiIn:Virtual Raw MIDI 1-0:VirMIDI 1-0 20:0 + Carla:MidiIn + + + diff --git a/voice/festivalrc b/textbeat/presets/festivalrc similarity index 100% rename from voice/festivalrc rename to textbeat/presets/festivalrc diff --git a/voice/test.xml b/textbeat/presets/test.xml similarity index 100% rename from voice/test.xml rename to textbeat/presets/test.xml diff --git a/textbeat/remote.py b/textbeat/remote.py new file mode 100644 index 0000000..7d95b1b --- /dev/null +++ b/textbeat/remote.py @@ -0,0 +1,3 @@ +from .defs import * + + diff --git a/textbeat/run.py b/textbeat/run.py new file mode 100644 index 0000000..46173e1 --- /dev/null +++ b/textbeat/run.py @@ -0,0 +1,4 @@ +from __future__ import absolute_import, unicode_literals, print_function, generators +# import textbeat +# def run(): +# textbeat.main() diff --git a/src/schedule.py b/textbeat/schedule.py similarity index 59% rename from src/schedule.py rename to textbeat/schedule.py index 85f61d8..32de82d 100644 --- a/src/schedule.py +++ b/textbeat/schedule.py @@ -1,19 +1,20 @@ -from . import * +from .defs import * -class Event: +class Event(object): def __init__(self, t, func, ch): self.t = t self.func = func self.ch = ch -class Schedule: +class Schedule(object): def __init__(self, ctx): self.ctx = ctx - self.events = [] # time,func,ch,skippable + self.events = [] # store this just in case logic() throws # we'll need to reenter knowing this value self.passed = 0.0 self.clock = 0.0 + self.last_clock = 0 self.started = False # all note mute and play events should be marked skippable def pending(self): @@ -21,12 +22,21 @@ def pending(self): def add(self, e): self.events.append(e) def clear(self): + assert False self.events = [] def clear_channel(self, ch): + assert False self.events = [ev for ev in self.events if ev.ch!=ch] def logic(self, t): processed = 0 - + self.passed = 0 + + # if self.last_clock == 0: + # self.last_clock = time.clock() + # clock = time.clock() + # self.dontsleep = (clock - self.last_clock) + # self.last_clock = clock + # clock = time.clock() # if self.started: # tdelta = (clock - self.passed) @@ -46,23 +56,31 @@ def logic(self, t): else: # sleep until next event if ev.t >= 0.0: - time.sleep(self.ctx.speed*t*(ev.t-self.passed)) + if self.ctx.cansleep and self.ctx.startrow == -1: + self.ctx.t += self.ctx.speed * t * (ev.t-self.passed) + time.sleep(max(0,self.ctx.speed * t * (ev.t-self.passed))) ev.func(0) self.passed = ev.t # only inc if positive else: ev.func(0) - - processed += 1 - slp = t*(1.0-self.passed) # remaining time + processed += 1 + + slp = t * (1.0 - self.passed) # remaining time if slp > 0.0: - time.sleep(self.ctx.speed*slp) + self.ctx.t += self.ctx.speed*slp + if self.ctx.cansleep and self.ctx.startrow == -1: + time.sleep(max(0,self.ctx.speed*slp)) self.passed = 0.0 + + self.events = self.events[processed:] + except KeyboardInterrupt: + self.events = self.events[processed:] + raise + except SignalError: self.events = self.events[processed:] - except KeyboardInterrupt as ex: - # don't replay events + raise + except EOFError: self.events = self.events[processed:] - raise ex - except: - QUITFLAG = True + raise diff --git a/textbeat/support.py b/textbeat/support.py new file mode 100644 index 0000000..487eff1 --- /dev/null +++ b/textbeat/support.py @@ -0,0 +1,63 @@ +from .defs import * +from shutilwhich import which +import tempfile, shutil +from . import instrument +# from xml.dom import minidom +ARGS = get_args() +SUPPORT = set(['midi']) +SUPPORT_ALL = set(['midi', 'fluidsynth', 'soundfonts']) # gme,mpe,sonicpi,supercollider,csound +MIDI = True +SOUNDFONTS = False # TODO: make this a SupportPlugin ref +AUTO = False +AUTO_MODULE = None +SOUNDFONT_MODULE = None +auto_inited = False + +SUPPORT_PLUGINS = {} + +# load plugins from plugins dir + +import textbeat.plugins as tbp +from textbeat.plugins import * +# search module exports for plugins +plugs = [] +for p in tbp.__dict__: + try: + pattr = getattr(tbp, p) + plugs += [pattr.export(ARGS)] + except: + pass +# plugs = instrument.plugins() +for plug in plugs: + # plug.init() + ps = plug.support() + SUPPORT_ALL = SUPPORT_ALL.union(ps) + if not plug.supported(): + continue + for s in ps: + SUPPORT.add(s) + SUPPORT_PLUGINS[s] = plug + if 'auto' in s: + AUTO = True + AUTO_MODULE = plug + auto_inited = True + if 'soundfonts' in s: + SOUNDFONTS = True + SOUNDFONT_MODULE = plug + +def supports(dev): + global SUPPORT + return dev in SUPPORT + +def supports_soundfonts(): + return SOUNDFONTS +def supports_auto(): + return AUTO +def supports(tech): + return tech in SUPPORT + +def support_stop(): + for plug in plugs: + if plug.inited(): + plug.stop() + diff --git a/src/theory.py b/textbeat/theory.py similarity index 79% rename from src/theory.py rename to textbeat/theory.py index 659b1f8..1ca4882 100644 --- a/src/theory.py +++ b/textbeat/theory.py @@ -2,13 +2,21 @@ import os, sys from future.utils import iteritems from collections import OrderedDict -from . import def_path -from . import load_def +from .defs import get_defs +from .parser import * FLATS=False SOLFEGE=False NOTENAMES=True # show note names instead of numbers +class NoSuchScale(BaseException): + pass +class NoSuchNote(BaseException): + pass + +NOTE_OFFSET_VALUES = [None,1,None,2,None,3,4,None,5,None,6,None,7] +# LETTER_OFFSET_VALUES = [None,'C',None,'D',None,'E','F',None,'G',None,'A',None,'B'] + SOLFEGE_NOTES ={ 'do': '1', 'di': '#1', @@ -67,14 +75,10 @@ def mode_name(self, idx): else: return self.name + " mode " + str(idx) return m - -DEFS = load_def('default') -for f in os.listdir(def_path()): - if f != 'default.yaml': - defs = load_def(f[:-len('.yaml')]) SCALES = {} MODES = {} +DEFS = get_defs() for k,v in iteritems(DEFS['scales']): scale = SCALES[k] = Scale(k, v['intervals']) i = 1 @@ -91,7 +95,7 @@ def mode_name(self, idx): # for lookup, normalize name first, add root to result # number chords can't be used with note numbers "C7 but not 17 # in the future it might be good to lint chord names in a test -# so that they dont clash with commands and break previous songs if chnaged +# so that they dont clash with commands and break previous songs if changed # This will be replaced for a better parser # TODO: need optional notes marked CHORDS = DEFS['chords'] @@ -180,3 +184,30 @@ def expand_chord(c): c = c[:-1] + 'm' return CHORDS[normalize_chord(c)].split(' ') +ALIGNED_NOTE_NAMES = [ + ['1','b2','2','b3','3','4','b5','5','b6','6','b7','7'], + ['','#1','','#2','','','#4','','#5','','#6',''], + ['C','Db','D','Eb','E','F','Gb','G','Ab','A','Bb','B'], + ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'], + ['bb2','bbb3','bb3','bb4','b4','bb5','bbb6','bb6','bbb7','bb7',''], + ['','','###1','##2','###2','##3','##4','###4','##5','###5','##6',''], + ['Dbb','Ebbb','Ebb','Fbb','Fb','Gbb','Abbb','Abb','Bbbb','Bbb',''], + ['','','C###','D##','D###','E##','F##','F###','G##','G###','A##',''] +] + +def note_offset(s): + n = 0 + sharps = count_seq(s,'#') + n += sharps + flats = count_seq(s,'b') + n -= flats + s = s[sharps + flats:] + if s: + s = s.lower() + for names in ALIGNED_NOTE_NAMES: + try: + return n + names.index(s) + except ValueError: + pass + raise NoSuchNote() + diff --git a/textbeat/track.py b/textbeat/track.py new file mode 100644 index 0000000..6b19f2f --- /dev/null +++ b/textbeat/track.py @@ -0,0 +1,424 @@ +from .defs import * +import math + +class Recording(object): + def __init__(self, name, slot): + self.name = slot + self.content = [] + +class Tuplet(object): + def __init__(self): + # self.tuplets = False + self.note_spacing = 1.0 + self.tuplet_count = 0 + self.tuplet_offset = 0.0 + +class Lane(object): + def __init__(self, ctx, idx, midich, parent=None): + self.idx = idx + self.midi = ctx.midi + self.ctx = ctx + self.schedule = self.ctx.schedule + def master(self): + return self.parent if self.parent else self + def reset(self): + self.notes = [0] * RANGE + self.sustain_notes = [False] * RANGE + +class Track(Lane): + class Flag: + ROMAN = bit(0) + # TRANSPOSE = bit(1) + FLAGS = [ + 'roman', # STUB: fit roman chord in scale shape + # 'transpose', # allow transposition of note letters + ] + def __init__(self, ctx, idx, midich): + Lane.__init__(self,ctx,idx,midich) + # self.midis = [player] + self.channels = [(0,midich)] + self.midich = midich # tracks primary midi channel + self.initial_channel = midich + self.non_drum_channel = midich + self.reset() + def us(self): + # microseconds + # return int(self.ctx.t)*1000000 + return math.floor(mido.second2tick(self.ctx.t, self.ctx.grid, self.ctx.tempo)) + def reset(self): + Lane.reset(self) + self.mode = 0 # 0 is NONE which inherits global mode + self.scale = None + # self.instrument = 0 + self.octave = 0 # rel to OCTAVE_BASE + self.modval = 0 # dont read in mod, just track its change by this channel + self.sustain = False # sustain everything? + self.arp_note = None # current arp note + self.arp_notes = [] # list of notes to arpegiate + self.arp_idx = 0 + self.arp_notes_left = 0 + self.arp_cycle_limit = 0 # cycles remaining, only if limit != 0 + self.arp_pattern = [] # relative steps to + self.arp_enabled = False + self.arp_once = False + self.arp_delay = 0.0 + self.arp_sustain = False + self.arp_note_spacing = 1.0 + # self.arp_reverse = False + self.vel = 100 + self.max_vel = -1 + self.soft_vel = -1 + self.ghost_vel = -1 + self.accent_vel = -1 + self.non_drum_channel = self.initial_channel + # self.off_vel = 64 + self.staccato = False + self.patch_num = 0 + self.transpose = 0 + self.pitchval = 0.0 + self.tuplet = [] # future + self.tuplets = False + self.note_spacing = 1.0 + self.tuplet_count = 0 + self.tuplet_offset = 0.0 + self.use_sustain_pedal = False # whether to use midi sustain instead of track + self.sustain_pedal_state = False # current midi pedal state + # self.schedule.clear_channel(self) + self.flags = 0 # set() + self.enabled = True + self.soloed = False + # self.muted = False + self.volval = 1.0 + self.slots = {} # slot -> Recording + self.slot = None # careful, this can be 0 + self_slot_idx = 0 + self.lane = None + self.lanes = [] + self.ccs = {} + self.dev = 0 + + # def _lazychannelfunc(self): + # # get active channel numbers + # return list(map(filter(lambda x: self.channels & x[0], [(1< 0 + # if f != f & FLAGS: + # raise ParseError('invalid flags') + self.flags |= f + def has_flags(self, f): + if isinstance(f, str): + f = 1 << FLAGS.index(f) + else: + assert f > 0 + # if f != f & FLAGS: + # raise ParseError('invalid flags') + return self.flags & f + def midifile_write(self, ch, msg): + # ch: midi channel index, not midi channel # (index 0 of self.channels tuple item) + while ch >= len(self.ctx.midifile.tracks): + self.ctx.midifile.tracks.append(mido.MidiTrack()) + # print(msg) + self.ctx.midifile.tracks[ch].append(msg) + def enable(self, v=True): + was = v + if not was and v: + self.enabled = v + self.panic() + def disable(self, v=True): + self.enable(not v) + def stop(self): + self.release_all(True) + for ch in self.channels: + status = (MIDI_CC<<4) + ch[1] + if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status,120,0)) + if self.ctx.midifile: + self.midifile_write(ch[0], mido.UnknownMetaMessage(status,data=[120, 0],time=self.us())) + else: + self.midi[ch[0]].write_short(status, 120, 0) + if self.modval>0: + self.refresh() + self.modval = False + def panic(self): + self.release_all(True) + for ch in self.channels: + status = (MIDI_CC<<4) + ch[1] + if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status,123,0)) + if self.ctx.midifile: + self.midifile_write(ch[0], mido.UnknownMetaMessage(status, [123, 0], time=self.us())) + else: + self.midi[ch[0]].write_short(status, 123, 0) + if self.modval>0: + self.refresh() + self.modval = False + def note_on(self, n, v=-1, sustain=False): + if self.use_sustain_pedal: + if sustain and self.sustain != sustain: + self.cc(MIDI_SUSTAIN_PEDAL, sustain) + elif not sustain: # sustain=False is overridden by track sustain + sustain = self.sustain + if v == -1: + v = self.vel + if n < 0 or n > RANGE: + return + for ch in self.channels: + self.notes[n] = v + self.sustain_notes[n] = sustain + # log("on " + str(n)) + if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: NOTE ON (%s, %s, %s)' % (n,v,ch)) + if (not self.ctx.muted or (self.ctx.muted and self.soloed))\ + and self.enabled and self.ctx.startrow==-1: + if self.ctx.midifile: + self.midifile_write(ch[0], mido.Message( + 'note_on',note=n,velocity=v,time=self.us(),channel=ch[1] + )) + else: + self.midi[ch[0]].note_on(n,v,ch[1]) + def note_off(self, n, v=-1): + if v == -1: + v = self.vel + if n < 0 or n >= RANGE: + return + if self.notes[n]: + # log("off " + str(n)) + for ch in self.channels: + if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: NOTE OFF (%s, %s, %s)' % (n,v,ch)) + if not self.ctx.midifile: + self.midi[ch[0]].note_on(self.notes[n],0,ch[1]) + self.midi[ch[0]].note_off(self.notes[n],v,ch[1]) + self.notes[n] = 0 + self.sustain_notes[n] = 0 + if self.ctx.midifile: + self.midifile_write(ch[0], mido.Message( + 'note_on',note=n,velocity=0,time=self.us(),channel=ch[1] + )) + self.midifile_write(ch[0], mido.Message( + 'note_off',note=n,velocity=v,time=self.us(),channel=ch[1] + )) + + self.cc(MIDI_SUSTAIN_PEDAL, True) + def release_all(self, mute_sus=False, v=-1): + if v == -1: + v = self.vel + for n in range(RANGE): + # if mute_sus, mute sustained notes too, otherwise ignore + mutesus_cond = True + if not mute_sus: + mutesus_cond = not self.sustain_notes[n] + if self.notes[n] and mutesus_cond: + for ch in self.channels: + if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: NOTE OFF (%s, %s, %s)' % (n,v,ch)) + self.midi[ch[0]].note_on(n,0,ch[1]) + self.midi[ch[0]].note_off(n,v,ch[1]) + self.notes[n] = 0 + self.sustain_notes[n] = 0 + # log("off " + str(n)) + # self.notes = [0] * RANGE + if self.modval>0: + self.cc(1,0) + # self.arp_enabled = False + # self.schedule.clear_channel(self) + # def cut(self): + def midi_channel(self, midich, stackidx=-1): + if midich==DRUM_CHANNEL: # setting to drums + if self.channels[stackidx][1] != DRUM_CHANNEL: + self.non_drum_channel = self.channels[stackidx][1] + self.octave = DRUM_OCTAVE + else: + for ch in self.channels: + if ch!=DRUM_CHANNEL: + midich = ch[1] + if midich != DRUMCHANNEL: # no suitable channel in span? + midich = self.non_drum_channel + if stackidx == -1: # all + self.release_all() + self.channels = [(0,midich)] + elif midich not in self.channels: + self.channels.append(midich) + def pitch(self, val): # [-1.0,1.0] + val = min(max(0,int((1.0 + val)*0x2000)),16384) + self.pitchval = val + val2 = (val>>0x7f) + val = val&0x7f + for ch in self.channels: + status = (MIDI_PITCH<<4) + ch[1] + if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: PITCH (%s, %s)' % (val,val2)) + if self.ctx.midifile: + self.midifile_write(ch[0],mido.UnknownMetaMessage(status,data=[val1,val2], time=self.us())) + else: + self.midi[ch[0]].write_short(status,val,val2) + def cc(self, cc, val): # control change + if type(val) ==type(bool): val = 127 if val else 0 # allow cc bool switches + for ch in self.channels: + status = (MIDI_CC<<4) + ch[1] + if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status, cc,val)) + if self.ctx.midifile: + self.midifile_write(ch[0], mido.UnknownMetaMessage(status,data=[cc,val],time=self.us())) + else: + self.midi[ch[0]].write_short(status,cc,val) + self.ccs[cc] = v + if cc==1: + self.modval = val + if cc==7: + self.volval = val/127.0 + def mod(self, val): + self.modval = 0 + return self.cc(1,val) + def patch(self, p, stackidx=-1): + if isinstance(p,basestring): + # look up instrument string in GM + i = 0 + inst = p.replace('_',' ').replace('.',' ').lower() + + if p in DRUM_WORDS: + self.midi_channel(DRUM_CHANNEL) + p = 0 + else: + if self.midich == DRUM_CHANNEL: + self.midi_channel(self.non_drum_channel) + + stop_search = False + gmwords = GM_LOWER + for w in inst.split(' '): + gmwords = list(filter(lambda x: w in x, gmwords)) + lengw = len(gmwords) + if lengw==1: + break + elif lengw==0: + log(FG.RED + 'Patch \"'+p+'\" not found') + assert False + assert len(gmwords) > 0 + if self.ctx.shell: + log(FG.GREEN + 'GM Patch: ' + STYLE.RESET_ALL + gmwords[0]) + p = GM_LOWER.index(gmwords[0]) + # for i in range(len(GM_LOWER)): + # continue_search = False + # for pword in inst.split(' '): + # if pword.lower() not in gmwords: + # continue_search = True + # break + # p = i + # stop_search=True + + # if stop_search: + # break + # if continue_search: + # assert i < len(GM_LOWER)-1 + # continue + + self.patch_num = p + # log('PATCH SET - ' + str(p)) + if stackidx==-1: + for ch in self.channels: + status = (MIDI_PROGRAM<<4) + ch[1] + if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: PROGRAM (%s, %s)' % (status,p)) + self.midi[ch[0]].write_short(status,p) + else: + status = (MIDI_PROGRAM<<4) + self.channels[stackidx][1] + if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: PROGRAM (%s, %s)' % (status,p)) + self.midi[self.channels[stackidx][0]].write_short(status,p) + + def bank(self, b): + self.ccs[0] = b + self.cc(0,b) + def arp(self, notes, count=0, sustain=False, pattern=[], reverse=False, octave=0): + self.arp_enabled = True + if reverse: + notes = notes[::-1] + self.arp_notes = list(map(lambda n: n + (octave*12), notes)) + self.arp_cycle_limit = count + self.arp_cycle = count + self.arp_pattern = pattern if pattern else [1] + self.arp_pattern_idx = 0 + self.arp_notes_left = len(notes) * max(1,count) + self.arp_idx = 0 # use inversions to move this start point (?) + self.arp_once = False + self.arp_sustain = sustain + def arp_stop(self): + self.arp_enabled = False + self.release_all() + def arp_next(self, stop_infinite=True): + stop = False + assert self.arp_enabled + # if not self.arp_enabled: + # self.arp_note = None + # return False + # out(self.arp_idx + 1) + if self.arp_notes_left != -1 or stop_infinite: + if self.arp_notes_left != -1: + self.arp_notes_left = max(0, self.arp_notes_left - 1) + if self.arp_notes_left <= 0: + if self.arp_cycle_limit or stop_infinite: + self.arp_note = None + self.arp_enabled = False + self.arp_note = self.arp_notes[self.arp_idx] + self.arp_idx = self.arp_idx + self.arp_pattern[self.arp_pattern_idx] + self.arp_pattern_idx = (self.arp_pattern_idx + 1) % len(self.arp_pattern) + self.arp_delay = (self.arp_delay + self.arp_note_spacing) - 1.0 + if self.arp_idx >= len(self.arp_notes) or self.arp_idx < 0: # cycle? + self.arp_once = True + if self.arp_cycle_limit: + self.arp_cycle -= 1 + if self.arp_cycle == 0: + self.arp_enabled = False + self.arp_idx = 0 + # else: + # self.arp_idx += 1 + return self.arp_note != None + def arp_restart(self, count = None): + self.arp_enabled = True + # self.arp_sustain = False + if count != None: # leave same (could be -1, so use -2) + self.arp_count = count + self.arp_idx = 0 + def tuplet_next(self): + delay = 0.0 + if self.tuplets: + delay = self.tuplet_offset + self.tuplet_offset += self.note_spacing - 1.0 + # if self.tuplet_offset >= 1.0 - EPSILON: + # out('!!!') + # self.tuplet_offset = 0.0 + self.tuplet_count -= 1 + if not self.tuplet_count: + self.tuplet_stop() + # else: + # self.tuplet_stop() + # if feq(delay,1.0): + # return 0.0 + # out(delay) + return delay + def tuplet_stop(self): + self.tuplets = False + self.tuplet_count = 0 + self.note_spacing = 1.0 + self.tuplet_offset = 0.0 + diff --git a/textbeat/tutorial.py b/textbeat/tutorial.py new file mode 100644 index 0000000..00aa961 --- /dev/null +++ b/textbeat/tutorial.py @@ -0,0 +1,12 @@ +from .defs import * + +class Tutorial(object): + def __init__(self, player): + self.player = player + player.shell = True + self.idx = 0 + def next(self): + pass + # print(MSG[self.idx]) + # self.idx += 1 + diff --git a/textbeat/tutorial.yaml b/textbeat/tutorial.yaml new file mode 100644 index 0000000..a03ae67 --- /dev/null +++ b/textbeat/tutorial.yaml @@ -0,0 +1,175 @@ +tutorial: + - Introduction: + - text: | + Welcome to the textbeat tutorial! + Make sure your sound is on. + First, let's make sure you can hear midi. + prompt: | + Type the the number 1 and hit enter. + answer: + - '1' + - text: | + Did you hear a note? It should probably sound like a low quality piano. + If you didn't, you need to check the manual for + setup instructions. Textbeat does not (yet) come with builtin sounds. + You have to have something else that will play the notes! + prompt: + Everything good? Type 1 to continue. + answer: + - '1' + - text: | + Alright, textbeat prefers note numbers. But if you really like letters, + you can use those too. By default, 1 is C. Try it! + prompt: + Type C to play the note. + answer: + - C + - text: | + You are typing into the textbeat(txbt) shell. + Usually, you'd write songs in textbeat files (.txbt), + but this is a good enough place to start and learn what + commands do what. + + There is one important difference between the shell and .txbt files. + + In the shell, you can type notes and they'll be played in sequence, + one after another. + But in .txbt files, everything is written vertically in columns, + so we can have different instruments on the same row. + + Let's play some notes in sequence. Type the notes below. + prompt: + '1 2 3' + - text: | + Woah, slow down there, Mozart. It's time to change the tempo. + Currently we're at 120bpm (beats per minute) with 4 grid subdivisions. + + The subdivisions make the note faster, so let's slow that down. + + Use the t and x global (%) commands to slow things down. + prompt: + '%t40x1' + - text: | + Alright, we're now at 40 beats per minute with no subdivisions From the top again! + prompt: + '1 2 3' + - text: | + Okay, let's try playing a scale. This is called the major scale. It goes + from 1 all the way up to 1 in a higher octave. We mark the higher octave + using \' + prompt: + "1 2 3 4 5 6 7 1'" + - text: | + We can use the higher octave with apostrophe (\') and lower with comma (,) + prompt: + "1, 1 1'" + - text: | + Let's stack 3 octaves of the same note. + + The slash implies moving down to the next octave. + + Musicians will hate me right now if I don't mention this. This + syntax definition is SPECIFIC to textbeat. There is something similar in music + called a slash chord (C/E). + + In textbeat, the slash just means to stack the notes or chords. + prompt: + "1/1/1" + - text: | + Let's suffix this with a '&' to hear the notes played in sequence. + + In music, we call this an arpeggio. We use the & symbol because it kind of + looks like an A for arpeggio. + prompt: + "1/1/1&" + - text: | + Alright, I'm bored of octaves, let's play a chord. + prompt: + 'sus' + - text: | + You just played a sus chord (also called sus4). Sus stands for suspended. + + It contains the notes 1, 4, and 5 (relative to where we position it). + + Like octaves, sus has a very pure sound. Let's arpeggiate it 3 times. + + You\'ve probably heard this chord before in piano runs. + prompt: + 'sus&3' + - text: | + Chords can be positioned wherever. If we don't write a number, that + just means its on 1, or the note C. + + We can move the whole chord by writing a note number or letter before it. + + Let's position it different places. + prompt: + '1sus 2sus 3sus' + - text: | + Let's introduce strum ($) chords. In textbeat speak, this just opens + them up and plays them all in a single grid space, + instead of walking them slowly like the arpeggio (&) command. + + We'll revisit strumming later, but until then, here it is. + prompt: + '1sus$ 2sus$ 3sus$' + - text: | + Alright, next, the major chord. Let's arpeggiate (&) it. + + Some people say major chords sound happy or accomplished. + + There are a few ways to write this one (ma, maj, major, etc.) + prompt: + 'ma&' + - text: | + Feeling happy yet? Maybe this one will fit your mood more. + + Here's a minor chord. + prompt: + 'm&' + - text: | + Minor sounds darker than major, maybe even sad. + + Alright let's walk some chords + + Some major and some minor. Pay attention! + + I also slipped in a cool new one. + prompt: + '1ma 2m 3m 4ma 5ma 6m 7o 1ma' + - text: | + So we're moving the chords up while changing the chord shapes + as we go to what sounds the best. + + It might be worth mentioning, you can do the same thing with roman numerals. + + The number implies position like the prefixes above, but they are + not notes by default, but major and minor chords depending on the case. + + The 'o' is short for diminished chord, and in our major scale you'll + find it on note 6. + + Let's play these chords! + prompt: + "I ii iii IV V vi viio I'" + - text: | + Alright, so you know that diminished chord of 7? + It probably sounded good in this context, but if you play it by + itself it sounds a little bit unresolved. Check it out. + prompt: + 'o&' + - text: | + Sus, major, minor, and diminished. That's a lot to take in. + + Let's end with one more command. The underscore (_) sustains the notes + like a piano pedal. + + Let's combine the strum and hold sustain commands ($_) and do + a nice piano run! + prompt: + 'ma/ma/1$_' + - text: | + It's probably time for you to go experiment with what you've learned. + + Come back for part II by typing tutorial2 in the shell. + diff --git a/txbt b/txbt new file mode 100755 index 0000000..60887c8 --- /dev/null +++ b/txbt @@ -0,0 +1,2 @@ +#!/bin/bash +PYTHONPATH=`dirname $0` python -m textbeat $* diff --git a/txbt.cmd b/txbt.cmd new file mode 100644 index 0000000..2561c00 --- /dev/null +++ b/txbt.cmd @@ -0,0 +1,3 @@ +@echo off +set PYTHONPATH=%~dp0 +py -3 -m textbeat %* \ No newline at end of file