From dced59a6820c442efa393bf3d0344e45f9201380 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Wed, 27 Jun 2018 09:00:43 -0700 Subject: [PATCH 01/59] fix pitch val and cc --- src/track.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/track.py b/src/track.py index 3bb9c38..bf9f180 100644 --- a/src/track.py +++ b/src/track.py @@ -42,7 +42,7 @@ def reset(self): self.staccato = False self.patch_num = 0 self.transpose = 0 - self.pitch = 0.0 + self.pitchval = 0.0 self.tuplets = False self.note_spacing = 1.0 self.tuplet_count = 0 @@ -142,7 +142,7 @@ def midi_channel(self, midich, stackidx=-1): 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 + self.pitchval = val val2 = (val>>0x7f) val = val&0x7f for ch in self.channels: @@ -159,7 +159,7 @@ def cc(self, cc, val): # control change if cc==1: self.modval = val def mod(self, val): - return cc(1,val) + return self.cc(1,val) def patch(self, p, stackidx=0): if isinstance(p,basestring): # look up instrument string in GM From 4244dea6db8b044c7db37b681ebff1542c341db7 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Wed, 27 Jun 2018 09:02:07 -0700 Subject: [PATCH 02/59] readme fixes --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2a2da7a..aeccf21 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,7 @@ In the future, articulation will be programmable, per-track or per-song. Notes of arpeggios can be modified as they're running, by having effects in a grid space, for example: -` +``` maj7& .? .? @@ -345,7 +345,7 @@ maj7& .? .? .? -` +``` maj7& starts a repeating 4-note arpeggio, and we indent to show this. @@ -402,7 +402,7 @@ Still working on this feature, it might be broken ``` :markername -@makername +@markername ``` Repeat counting, callstack, etc. coming shortly. Code almost done. @@ -462,7 +462,7 @@ To set the notes to match scale, %r=2 ``` -# And here's what it looks like +# And here's what it all looks like ``` %t=120x4 p=piano,bass,drums c=20,-2 From adc3dbbbac521cfa3ff41ab80bffa021c22deeaa Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Wed, 27 Jun 2018 09:07:57 -0700 Subject: [PATCH 03/59] transposition explanation --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aeccf21..0305e26 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,10 @@ Both Tempo and Grid can be decimal numbers as well. Notice the bottom line has an extra apostrophe character ('). This plays 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 From 1e39d1998e92b405fe59ce365e430fc61437279e Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Wed, 27 Jun 2018 10:30:42 -0700 Subject: [PATCH 04/59] reworked tutorial, added back in adv info that was removed --- README.md | 307 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 199 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index 0305e26..bb1a612 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,15 @@ Copyright (c) 2018 Grady O'Connell - [Project Board](https://trello.com/b/S8AsJaaA/decadence) - Vim integration: [vim-decadence](https://github.com/flipcoder/vim-decadence) +**This project is still very new. Despite number of features, you may quickly +run into issues, especially with editor integration.** + # Overview 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: @@ -56,42 +56,7 @@ If you're on Linux, you can use soundfonts through qsynth or use a software inst 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. -# Notes and Chords - -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. - -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. - -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" - -``` -# 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) -``` - -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)) - -# The Basics +# 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 @@ -130,9 +95,30 @@ The grid is the beat/quarter-note subdivision. Both Tempo and Grid can be decimal numbers as well. -## Transposition and Octaves +## 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, decadence 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 -Notice the bottom line has an extra apostrophe character ('). This plays the note in the next octave +In the first example, the apostrophe character (') was used to play the note in the next octave. For an octave below, use a comma (,). Repeat these for additional octaves (,,, for 3 down, '' for 2 up, etc). @@ -214,9 +200,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: @@ -232,10 +221,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. @@ -266,7 +257,7 @@ To strum, use the hold (_) symbol with this. maj$_ ``` -# Accents +## Velocity and Accents Use a ! or ? to accent or soften a note respectively. @@ -294,7 +285,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 @@ -309,13 +300,13 @@ 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 - @@ -327,14 +318,19 @@ Interpolation not yet impl - ``` -# Articulation +Unlike accents, volume changes persist. + +Interpolation is not yet impl + +## Articulation + +The vibrato symbol is 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: @@ -356,7 +352,7 @@ Certain notes of the sequence are modulated with short/staccato '.', soft '?' an 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 @@ -386,7 +382,7 @@ 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). @@ -397,7 +393,7 @@ across the tracks. The midi names support partial matches (case-insensitive). For a full list of GM names, see [config/gm.yaml](https://github.com/flipcoder/decadence/blob/master/config/gm.yaml). -# Markers +## Markers Still working on this feature, it might be broken @@ -410,7 +406,7 @@ Still working on this feature, it might be broken Repeat counting, callstack, etc. coming shortly. Code almost done. -# Tuplets +## Tuplets Very early support for this. See tuplet.dc example. The 't' command spreads a set of notes across a tuplet grid, @@ -438,11 +434,11 @@ Consider the 2 tracks: 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. -# Picking +## Picking [Currently designing this feature](https://trello.com/c/D01rlTWp/26-picking) -# Key changes +## Key changes The following behavior is optional and probably not useful to many musicians. @@ -465,66 +461,161 @@ To set the notes to match scale, %r=2 ``` -# And here's what it all looks like +## Chords (Advanced) -``` -%t=120x4 p=piano,bass,drums c=20,-2 +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)) -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 in the test/ folder. Play them with decadence from the +command line: -These lists does not include certain chord modifications (add, no, drop, etc.) +``` +./decadence.py test/jazz.dc +``` # Advanced +## 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) + - 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 +- :: set marker (requires name) +- @: go back to last marker, or start +- @@: pop mark, go back to last area +- @start: return to start +- @end: end song + +Future: Repeat and region markers may be changed to '|:' and ':|' symbols. +``` + +## 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 +- ch: assign track to a midi channel + - midi channels exceeding max value will be spanned across outputs +- pc: program assign + - Set program to a given number + - Global var (%) p is usually prefered for string matching +- cc: control change (midi CC param) + - setting CC5 to 25 would be c5:25 +- bs: bank select (not impl) +- ~: 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 + +Note: Fractional values specified are formated like numbers after a decimal point: +Example: 3, 30, and 300 all mean 30% (read like .3, .30, etc.) +``` + +## 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/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). + +These lists does not include certain chord modifications (add, no, drop, etc.). # What else? From 7421b85126cd3b306b8506217817651fd8b3cb3f Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Wed, 27 Jun 2018 11:57:55 -0700 Subject: [PATCH 05/59] more reorg, defs merge fix, readme fix --- .gitignore | 1 + README.md | 18 ++++--- config/gm.yaml | 128 ----------------------------------------------- def/gm.yaml | 129 ++++++++++++++++++++++++++++++++++++++++++++++++ src/__init__.py | 14 +++++- src/context.py | 2 + src/midi.py | 4 +- src/schedule.py | 7 ++- src/theory.py | 9 +--- 9 files changed, 163 insertions(+), 149 deletions(-) delete mode 100644 config/gm.yaml create mode 100644 def/gm.yaml diff --git a/.gitignore b/.gitignore index 0d20b64..7a60b85 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +__pycache__/ *.pyc diff --git a/README.md b/README.md index bb1a612..48867b9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ![Decadence](icon.png) decadence -Plaintext music tracker and midi shell. +Plaintext music sequencer and interactive shell. Write music in vim or your favorite text editor. @@ -131,10 +131,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 @@ -354,7 +354,9 @@ For staccato usage w/o a note name, an extra dot is required since '.' is simply ## 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,8 +369,8 @@ Columns are separate tracks, line them up for more than one instrument ``` 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), +specify the column width manually at the top, +which allows vim to mark the columns. ``` # sets column width to 8 @@ -385,13 +387,13 @@ For best view in an editor, it is recommended that you offset the first column b ## 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 partial case-insensitive matches. ``` %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/decadence/blob/master/config/gm.yaml). ## Markers 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/def/gm.yaml b/def/gm.yaml new file mode 100644 index 0000000..7a3ab5c --- /dev/null +++ b/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/src/__init__.py b/src/__init__.py index 4767e63..86439de 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -7,7 +7,7 @@ import yaml, colorama, appdirs from docopt import docopt from collections import OrderedDict -import pygame, pygame.midi as midi +import pygame, pygame.midi from multiprocessing import Process,Pipe from prompt_toolkit import prompt from prompt_toolkit.styles import style_from_dict @@ -32,7 +32,6 @@ RANGE = 109 OCTAVE_BASE = 5 DRUM_WORDS = ['drum','drums','drumset','drumkit','percussion'] -SPEECH_WORDS = ['speech','say','speak'] CCHAR = ' <>=~.\'`,_&|!?*\"$(){}[]%' CCHAR_START = 'T' # control chars PRINT = True @@ -129,6 +128,17 @@ def load_def(fn): random.seed() +DEFS = load_def('default') +for f in os.listdir(def_path()): + if f != 'default.yaml': + defs = load_def(f[:-len('.yaml')]) + c = defs.copy() + c.update(DEFS) + DEFS = c + +def get_defs(): + return DEFS + from .schedule import * from .parser import * from .theory import * diff --git a/src/context.py b/src/context.py index 2075850..0f5f8f5 100644 --- a/src/context.py +++ b/src/context.py @@ -13,6 +13,7 @@ def __init__(self): self.bcproc = None self.log = False self.canfollow = False + self.cansleep = True self.lint = False self.tracks_active = 1 self.showmidi = False @@ -45,6 +46,7 @@ def __init__(self): self.speed = 1.0 self.player = None self.instrument = None + self.t = 0.0 def follow(self, count): if self.canfollow: diff --git a/src/midi.py b/src/midi.py index 428aaf2..93984d0 100644 --- a/src/midi.py +++ b/src/midi.py @@ -1,7 +1,7 @@ -from . import load_cfg +from . import * MIDI_CC = 0B1011 MIDI_PROGRAM = 0B1100 MIDI_PITCH = 0B1110 -GM = load_cfg('gm') +GM = get_defs()['patches'] GM_LOWER = [""]*len(GM) for i in range(len(GM)): GM_LOWER[i] = GM[i].lower() diff --git a/src/schedule.py b/src/schedule.py index 85f61d8..6b40a6a 100644 --- a/src/schedule.py +++ b/src/schedule.py @@ -46,7 +46,8 @@ 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: + time.sleep(self.ctx.speed*t*(ev.t-self.passed)) ev.func(0) self.passed = ev.t # only inc if positive else: @@ -56,7 +57,8 @@ def logic(self, t): slp = t*(1.0-self.passed) # remaining time if slp > 0.0: - time.sleep(self.ctx.speed*slp) + if self.ctx.cansleep: + time.sleep(self.ctx.speed*slp) self.passed = 0.0 self.events = self.events[processed:] except KeyboardInterrupt as ex: @@ -64,5 +66,6 @@ def logic(self, t): self.events = self.events[processed:] raise ex except: + # log('shedule ex: ') QUITFLAG = True diff --git a/src/theory.py b/src/theory.py index 659b1f8..a883085 100644 --- a/src/theory.py +++ b/src/theory.py @@ -2,8 +2,7 @@ import os, sys from future.utils import iteritems from collections import OrderedDict -from . import def_path -from . import load_def +from . import get_defs FLATS=False SOLFEGE=False @@ -67,14 +66,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 From 38bc8fc800950b760096cc0e030d83cc7871f730 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Wed, 27 Jun 2018 18:04:07 -0700 Subject: [PATCH 06/59] cleaning, reworking --- config/def.yaml | 247 ---------------------------------------------- decadence.py | 39 +++++--- def/dc.yaml | 35 ++++--- def/default.yaml | 50 +--------- def/informal.yaml | 11 ++- src/context.py | 5 +- src/track.py | 1 + 7 files changed, 63 insertions(+), 325 deletions(-) delete mode 100644 config/def.yaml diff --git a/config/def.yaml b/config/def.yaml deleted file mode 100644 index 93412f0..0000000 --- a/config/def.yaml +++ /dev/null @@ -1,247 +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' - - # 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' - -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 - # 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 - p: pow - diff --git a/decadence.py b/decadence.py index 2eb584a..50d2600 100755 --- a/decadence.py +++ b/decadence.py @@ -85,7 +85,7 @@ 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 == '--remote': dc.remote = True elif arg == '--lint': LINT = True elif arg == '--quiet': set_print(False) elif arg == '--follow': @@ -140,6 +140,8 @@ dc.dcmode = '' dc.shell = True +dc.interactive = dc.shell or dc.remote + pygame.midi.init() if pygame.midi.get_count()==0: print('No midi devices found.') @@ -149,6 +151,7 @@ port = pygame.midi.get_device_info(i) portname = port[1].decode('utf-8') # timidity + # print(portname) devs = [ 'timidity port 0', 'synth input port', @@ -244,7 +247,7 @@ # 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 + if dc.interactive 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: @@ -255,7 +258,7 @@ dc.line = '.' if not arps_remaining and not dc.schedule.pending(): - if dc.shell or dc.daemon: + if dc.interactive: for ch in dc.tracks[:dc.tracks_active]: ch.release_all() @@ -274,14 +277,16 @@ # if bufline.endswith('.dc'): # play file? # bufline = raw_input(cline) - bufline = prompt(cline, - history=HISTORY, vi_mode=dc.vimode) + 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: + elif dc.remote: pass - # wait on socket + else: + assert False + + dc.buf += bufline + continue else: @@ -576,7 +581,7 @@ 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 '' @@ -602,6 +607,12 @@ 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) @@ -612,11 +623,11 @@ 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, @@ -1016,6 +1027,10 @@ 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 = [] + if ignore: allnotes = [] notes = [] @@ -1305,7 +1320,7 @@ if dc.showtext: showtext.append('soften(??)') elif c=='?': # soft - if ch.quiet_vel>0: + if ch.soft_vel >= 0: vel = ch.soft_vel else: vel = max(0,int(ch.vel*0.5)) diff --git a/def/dc.yaml b/def/dc.yaml index c0dbea0..f8244b4 100644 --- a/def/dc.yaml +++ b/def/dc.yaml @@ -1,27 +1,34 @@ chords: - # experimental voicings specific to dc, just for fun + # 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 - # majadd2 - wu: '3 4 5' # maadd4 - wu7: '3 4 5 7' # ma7add4 + # majadd4 + wu: '3 4 5' + wu7: '3 4 5 7' wu7b5: '2 3 b5 7' - wu-: 'b3 4 5' # madd4 + wu-: 'b3 4 5' wu-7: 'b3 4 5 7' - wu-7b5: 'b3 4 b5 7' + wu-7b5: 'b3 4 b5 7' - # extended sus voicings - 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+: '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 +# unused, future reference +shorthand: + wu: + replace: ma + add: '4' + wu-: + replace: m + add: '4' + wu+: + replace: + + add: '4' + +chord_alts: + sq: sus24 + diff --git a/def/default.yaml b/def/default.yaml index ce50bea..a8cd3be 100644 --- a/def/default.yaml +++ b/def/default.yaml @@ -191,58 +191,14 @@ chords: 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: + + aug7: '7+' ma#5: + ma#5: + - aug7: +7 p4: '4' p5: '5' -: m @@ -257,7 +213,7 @@ chord_alts: plyd7: ma7#4 # Madd9: maadd9 # maor7: ma7 - Mb5: mab5 + # Mb5: mab5 # M7: ma7 # M7b5: ma7b5 # min: m @@ -265,7 +221,6 @@ chord_alts: # min7: m7 # minor7: m7 p: pow - # 11th: 11 o: dim o7: dim7 7o: dim7 @@ -273,7 +228,6 @@ chord_alts: 9o: dim9 o11: dim11 11o: dim11 - sus24: sq # mma7: mm7 # mma9: mm9 # mma11: mm11 diff --git a/def/informal.yaml b/def/informal.yaml index 5847a9d..1de16db 100644 --- a/def/informal.yaml +++ b/def/informal.yaml @@ -12,6 +12,13 @@ chords: # 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' + sus7: '4 5 b7' + sus9: '4 5 b7 9' + sus24: '2 4 5' + +# for future reference +shorthand: + mu: + replace: ma + add: '2' diff --git a/src/context.py b/src/context.py index 0f5f8f5..0e97d35 100644 --- a/src/context.py +++ b/src/context.py @@ -39,8 +39,9 @@ def __init__(self): self.dcmode = 'n' # n normal c command s sequence self.schedule = Schedule(self) self.tracks = [] - self.shell = True - self.daemon = False + self.shell = False + self.remote = False + self.interactive = False self.gui = False self.portname = '' self.speed = 1.0 diff --git a/src/track.py b/src/track.py index bf9f180..dcc6aa0 100644 --- a/src/track.py +++ b/src/track.py @@ -12,6 +12,7 @@ def __init__(self, ctx, idx, midich, player, schedule): self.midich = midich # tracks primary midi channel self.initial_channel = midich self.non_drum_channel = midich + # self.strings = [] self.reset() def reset(self): self.notes = [0] * RANGE From 02b2af5aa5bba5894094c526562abf72a1a04c18 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Wed, 27 Jun 2018 18:45:19 -0700 Subject: [PATCH 07/59] add chords now parse correctly --- decadence.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/decadence.py b/decadence.py index 50d2600..e06e3e0 100755 --- a/decadence.py +++ b/decadence.py @@ -818,7 +818,7 @@ nonotes = [] chordname = '' reverse = False - addhigherroot = False + # addhigherroot = False # cut chord name from text after it for char in tok: @@ -829,9 +829,9 @@ if char == '\\': reverse = True break - if char == '^': - addhigherroot = True - break + # if char == '^': + # addhigherroot = True + # break chordname += char addnotes = [] try: @@ -853,11 +853,7 @@ 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 @@ -882,13 +878,20 @@ # log(chordname) # don't include tuplet in chordname + if 'add' in chordname: + print(chordname) + addtoks = chordname.split('add') + print(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 chordname: #and not chordname[1:] in 'bcdef': if roman == -1: # minor if chordname[0] in '6719': chordname = 'm' + chordname From fdaa4834b6a0f0a4344dbc5127185a568db805b8 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Wed, 27 Jun 2018 18:46:13 -0700 Subject: [PATCH 08/59] removed print statements left in --- decadence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/decadence.py b/decadence.py index e06e3e0..4123d23 100755 --- a/decadence.py +++ b/decadence.py @@ -879,9 +879,9 @@ # log(chordname) # don't include tuplet in chordname if 'add' in chordname: - print(chordname) + # print(chordname) addtoks = chordname.split('add') - print(addtoks) + # print(addtoks) chordname = addtoks[0] addnotes = addtoks[1:] From e59845af4314d5a2413330434bd833e6325655a4 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Thu, 28 Jun 2018 12:41:06 -0700 Subject: [PATCH 09/59] follow fix, defs merge() --- decadence.py | 36 +++++++++++++++++------------ def/dc.yaml | 21 ++++++++--------- def/default.yaml | 3 +-- def/informal.yaml | 9 ++++---- src/__init__.py | 58 +++++++++++++++++++++++++++++++++++++++-------- 5 files changed, 84 insertions(+), 43 deletions(-) diff --git a/decadence.py b/decadence.py index 4123d23..2a8bf26 100755 --- a/decadence.py +++ b/decadence.py @@ -8,8 +8,8 @@ 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 [--ring | --follow | --csound | --sonic-pi] [-eftnpsrxh] [SONGNAME] + decadence.py [+RANGE] [--ring || --follow | --csound | --sonic-pi] [-eftnpsrxh] [SONGNAME] decadence.py -c [COMMANDS ...] decadence.py -l [LINE_CONTENT ...] @@ -29,7 +29,7 @@ + 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) + -h --transpose transpose (in half steps) --sustain sustain by default --numbers use note numbers in output --notenames use note names in output @@ -89,8 +89,8 @@ elif arg == '--lint': LINT = True elif arg == '--quiet': set_print(False) elif arg == '--follow': - set_print(True) - dc.canfollow = False + set_print(False) + dc.canfollow = True elif arg == '--flats': FLATS = True elif arg == '--sharps': SHARPS= True elif arg == '--edit': pass @@ -214,7 +214,7 @@ pass # no stop param if dc.shell: - log(FG.BLUE + 'decadence v'+str(VERSION)) + log(FG.BLUE + 'decadence')# v'+str(VERSION)) log('Copyright (c) 2018 Grady O\'Connell') log('https://github.com/flipcoder/decadence') active = SUPPORT_ALL & SUPPORT @@ -271,7 +271,7 @@ # 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) + ' ' +\ + note_name(dc.transpose + 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'): @@ -342,7 +342,7 @@ if tok[0]==' ': tok = tok[1:] var = tok[0].upper() - if var in 'TGNPSRMCX': + if var in 'TGNPSRMCXK': cmd = tok.split(' ')[0] op = cmd[1] try: @@ -406,6 +406,10 @@ elif var=='F': # flags for i in range(len(vals)): dc.tracks[i].add_flags(val.split(',')) + elif var=='K': + dc.transpose = int(val) + # for ch in TRACKS: + # ch.transpose = int(val) elif var=='R' or var=='S': try: if val: @@ -416,15 +420,15 @@ modescale = (dc.scale.name,int(val)) else: alts = {'major':'ionian','minor':'aeolian'} - try: - modescale = (alts[modescale[0]],modescale[1]) - except: - pass + # try: + # modescale = (alts[val[0],val[1]) + # except KeyError: + # pass val = val.lower().replace(' ','') try: modescale = MODES[val] - except: + except KeyError: raise NoSuchScale() try: @@ -432,8 +436,8 @@ dc.mode = modescale[1] inter = dc.scale.intervals dc.transpose = 0 + # log(dc.mode-1) - log(dc.mode-1) if var=='R': for i in range(dc.mode-1): inc = 0 @@ -442,6 +446,8 @@ except ValueError: pass dc.transpose += inc + elif var=='S': + pass except ValueError: raise NoSuchScale() else: @@ -884,7 +890,7 @@ # print(addtoks) chordname = addtoks[0] addnotes = addtoks[1:] - + if chordname.endswith('T'): chordname = chordname[:-1] cut -= 1 diff --git a/def/dc.yaml b/def/dc.yaml index f8244b4..af75a4d 100644 --- a/def/dc.yaml +++ b/def/dc.yaml @@ -17,17 +17,16 @@ chords: 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 -# unused, future reference -shorthand: - wu: - replace: ma - add: '4' - wu-: - replace: m - add: '4' - wu+: - replace: + - add: '4' +#shorthand: +# wu: +# replace: ma +# add: '4' +# wu-: +# replace: m +# add: '4' +# wu+: +# replace: + +# add: '4' chord_alts: sq: sus24 diff --git a/def/default.yaml b/def/default.yaml index a8cd3be..9bf58f7 100644 --- a/def/default.yaml +++ b/def/default.yaml @@ -198,7 +198,6 @@ chord_alts: aug: + aug7: '7+' ma#5: + - ma#5: + p4: '4' p5: '5' -: m @@ -210,7 +209,7 @@ chord_alts: lyd: mab5 lyd7: ma7b5 plyd: ma#4 - plyd7: ma7#4 + lyd5: ma#4 # Madd9: maadd9 # maor7: ma7 # Mb5: mab5 diff --git a/def/informal.yaml b/def/informal.yaml index 1de16db..56e32b6 100644 --- a/def/informal.yaml +++ b/def/informal.yaml @@ -16,9 +16,8 @@ chords: sus9: '4 5 b7 9' sus24: '2 4 5' -# for future reference -shorthand: - mu: - replace: ma - add: '2' +#shorthand: +# mu: +# replace: ma +# add: '2' diff --git a/src/__init__.py b/src/__init__.py index 86439de..069fea0 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,12 +1,12 @@ #!/usr/bin/python from __future__ 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 from multiprocessing import Process,Pipe from prompt_toolkit import prompt @@ -18,7 +18,7 @@ if sys.version_info[0]==3: basestring = str -VERSION = '0.1' +# VERSION = '0.1' FG = colorama.Fore BG = colorama.Back STYLE = colorama.Style @@ -82,6 +82,7 @@ def get_args(): SCRIPT_PATH = os.path.dirname(os.path.realpath(os.path.join(__file__,'..'))) CFG_PATH = os.path.join(SCRIPT_PATH, 'config') DEF_PATH = os.path.join(SCRIPT_PATH, 'def') +DEF_EXT = '.yaml' def cfg_path(): return CFG_PATH def def_path(): @@ -128,13 +129,50 @@ def load_def(fn): random.seed() -DEFS = load_def('default') -for f in os.listdir(def_path()): - if f != 'default.yaml': - defs = load_def(f[:-len('.yaml')]) - c = defs.copy() - c.update(DEFS) - DEFS = c +class Diff: + NONE = 0 + ADD = 1 + REMOVE = 2 + UPDATE = 3 + +def merge(a, b, overwrite=False, skip=None, diff=None, pth=None): + for k,v in iteritems(b): + contains = k in a + if contains and isinstance(a[k], dict) and isinstance(b[k], collections.Mapping): + loc = (pth+[k]) if pth else None + if callable(skip): + if not skip(loc,v): + merge(a[k],b[k], overwrite, skip, diff, loc) + else: + merge(a[k],b[k], overwrite, skip, diff, loc) + else: + if contains: + if callable(overwrite): + loc = (pth+[k]) if pth!=None else k + if overwrite(loc,v): + a[k] = b[k] + if diff!=None: + diff.add((Diff.UPDATE,loc,v)) + else: + pass + elif overwrite: + if diff!=None: + old = copy.copy(a[k]) + a[k] = b[k] + if diff!=None: + loc = (pth+[k]) if pth!=None else k + diff.add((Diff.UPDATE,loc,v,old)) + else: + a[k] = b[k] + if diff!=None: + loc = (pth+[k]) if pth!=None else k + diff.add((Diff.ADD,loc,v)) + return a + +DEFS = {} +for f in os.listdir(DEF_PATH): + if f.lower().endswith(DEF_EXT): + merge(DEFS,load_def(f[:-len(DEF_EXT)])) def get_defs(): return DEFS From e69409fcc3e106ed3df81b92467bb15e8b2f929b Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Thu, 28 Jun 2018 13:26:52 -0700 Subject: [PATCH 10/59] remote: better line following --- decadence.py | 14 ++++++-------- src/context.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/decadence.py b/decadence.py index 2a8bf26..9f7455b 100755 --- a/decadence.py +++ b/decadence.py @@ -107,12 +107,12 @@ if ARGS['SONGNAME']: FN = ARGS['SONGNAME'] with open(FN) as f: + lc = 0 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': + elif len(line)>=2 and line[-2:0] == '\r\n': line = line[:-2] # if not line: @@ -134,6 +134,7 @@ lc += 1 dc.buf += [line] + # dc.rowno.append(lc) dc.shell = False else: if dc.dcmode == 'n': @@ -235,6 +236,8 @@ header = True # set this to false as we reached cell data while not dc.quitflag: + dc.follow() + try: dc.line = '.' try: @@ -314,7 +317,6 @@ if dc.line: # COMMENTS (;) if dc.line[0] == ';': - dc.follow(1) dc.row += 1 continue @@ -322,14 +324,12 @@ 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 @@ -457,7 +457,6 @@ print(FG.RED + 'No such scale.') pass - dc.follow(1) dc.row += 1 continue @@ -1476,7 +1475,7 @@ i += 1 cell_idx += 1 - + while True: try: if not ctrl and not header: @@ -1514,7 +1513,6 @@ if not dc.shell and not dc.pause(): break - dc.follow(1) dc.row += 1 # TODO: turn all midi note off diff --git a/src/context.py b/src/context.py index 0e97d35..4a40745 100644 --- a/src/context.py +++ b/src/context.py @@ -35,6 +35,7 @@ def __init__(self): self.track_history = ['.'] * NUM_TRACKS self.fn = None self.row = 0 + # self.rowno = [] self.stoprow = -1 self.dcmode = 'n' # n normal c command s sequence self.schedule = Schedule(self) @@ -48,10 +49,15 @@ def __init__(self): self.player = None self.instrument = None self.t = 0.0 + self.last_follow = 0 - def follow(self, count): + def follow(self): if self.canfollow: - print('\n' * max(0,count-1)) + cursor = self.row + 1 + if cursor != self.last_follow: + print(cursor) + self.last_cursor = cursor + # print(self.rowno[self.row]) def pause(self): try: From 20ec2f72b16c39ada38a4074a6dca2a2ad571814 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Thu, 28 Jun 2018 15:18:09 -0700 Subject: [PATCH 11/59] contextual play, started solo/mute channels --- decadence.py | 28 +++++++++++++++++----------- src/context.py | 8 +++++--- src/schedule.py | 8 ++++---- src/track.py | 15 +++++++++++++-- 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/decadence.py b/decadence.py index 9f7455b..495a228 100755 --- a/decadence.py +++ b/decadence.py @@ -195,10 +195,10 @@ if arg.startswith('+'): vals = arg[1:].split(',') try: - dc.row = int(vals[0]) + dc.startrow = int(vals[0]) except ValueError: try: - dc.row = dc.markers[vals[0]] + dc.startrow = dc.markers[vals[0]] except KeyError: log('invalid entry point') dc.quitflag = True @@ -242,6 +242,8 @@ dc.line = '.' try: dc.line = dc.buf[dc.row] + if dc.row == dc.startrow: + dc.startrow = -1 if dc.stoprow!=-1 and dc.row == dc.stoprow: dc.buf = [] raise IndexError @@ -571,18 +573,18 @@ if cell and cell[0]=='-': if dc.shell: - ch.mute() + ch.stop() else: ch.release_all() # don't mute sustain cell_idx += 1 continue - if cell and cell[0]=='=': # hard mute - ch.mute() + if cell and cell[0]=='=': # hard stop + ch.stop() cell_idx += 1 continue - if cell and cell[0]=='-': # mute prefix + if cell and cell[0]=='-': # stop prefix ch.release_all(True) # ch.sustain = False cell = cell[1:] @@ -1068,7 +1070,7 @@ cell = cell.strip() # ignore spaces vel = ch.vel - mute = False + stop = False sustain = ch.sustain delay = 0.0 @@ -1183,10 +1185,14 @@ ch.mod(127) cell = cell[1:] # dc.sustain - elif cell.startswith('__-'): - ch.mute() - sustain = ch.sustain = True - cell = cell[3:] + elif cell.startswith('--'): + ch.stop() + sustain = ch.sustain = False + cell = cell[2:] + elif cell.startswith('=='): + ch.panic() + sustain = ch.sustain = False + cell = cell[2:] elif c2=='__': sustain = ch.sustain = True cell = cell[2:] diff --git a/src/context.py b/src/context.py index 4a40745..b8707a6 100644 --- a/src/context.py +++ b/src/context.py @@ -36,6 +36,7 @@ def __init__(self): self.fn = None self.row = 0 # self.rowno = [] + self.startrow = -1 self.stoprow = -1 self.dcmode = 'n' # n normal c command s sequence self.schedule = Schedule(self) @@ -46,13 +47,14 @@ def __init__(self): self.gui = False self.portname = '' self.speed = 1.0 + self.muted = False # mute all except for solo tracks self.player = None self.instrument = None self.t = 0.0 self.last_follow = 0 - + def follow(self): - if self.canfollow: + if self.startrow==-1 and self.canfollow: cursor = self.row + 1 if cursor != self.last_follow: print(cursor) @@ -66,5 +68,5 @@ def pause(self): input(' === PAUSED === ') except: return False - return True + return True diff --git a/src/schedule.py b/src/schedule.py index 6b40a6a..bc199aa 100644 --- a/src/schedule.py +++ b/src/schedule.py @@ -26,7 +26,7 @@ def clear_channel(self, ch): self.events = [ev for ev in self.events if ev.ch!=ch] def logic(self, t): processed = 0 - + # clock = time.clock() # if self.started: # tdelta = (clock - self.passed) @@ -46,18 +46,18 @@ def logic(self, t): else: # sleep until next event if ev.t >= 0.0: - if self.ctx.cansleep: + if self.ctx.cansleep and self.ctx.startrow == -1: time.sleep(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 if slp > 0.0: - if self.ctx.cansleep: + if self.ctx.cansleep and self.ctx.startrow == -1: time.sleep(self.ctx.speed*slp) self.passed = 0.0 self.events = self.events[processed:] diff --git a/src/track.py b/src/track.py index dcc6aa0..51bb204 100644 --- a/src/track.py +++ b/src/track.py @@ -52,6 +52,8 @@ def reset(self): self.sustain_pedal_state = False # current midi pedal state self.schedule.clear_channel(self) self.flags = set() + self.enabled = True + self.soloed = False # def _lazychannelfunc(self): # # get active channel numbers # return list(map(filter(lambda x: self.channels & x[0], [(1< Date: Thu, 28 Jun 2018 20:19:59 -0700 Subject: [PATCH 12/59] markers/repeats/callstack, more test songs, persist vol --- decadence.py | 148 ++++++++++++++++++++++++++++++++++++------------ src/context.py | 15 +++-- src/track.py | 14 ++++- test/1.dc | 22 +++++++ test/2.dc | 37 ++++++++++++ test/a.dc | 25 ++++++++ test/markers.dc | 23 ++++++-- test/new.dc | 27 +++++++++ 8 files changed, 264 insertions(+), 47 deletions(-) create mode 100644 test/1.dc create mode 100644 test/2.dc create mode 100644 test/a.dc create mode 100644 test/new.dc diff --git a/decadence.py b/decadence.py index 495a228..df9fcea 100755 --- a/decadence.py +++ b/decadence.py @@ -106,6 +106,7 @@ # FN = sys.argv[-1] if ARGS['SONGNAME']: FN = ARGS['SONGNAME'] + # dc.markers[''] = 0 # start marker with open(FN) as f: lc = 0 for line in f.readlines(): @@ -234,6 +235,9 @@ log('Read the manual and look at examples. Have fun!') log('') +for ch in dc.tracks: + ch.refresh() + header = True # set this to false as we reached cell data while not dc.quitflag: dc.follow() @@ -323,15 +327,21 @@ 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.row += 1 - continue - # continue - elif dc.line[0]==':': #prefix marker + # if dc.line[-1]==':': # suffix marker + # # allow override of markers in case of reuse + # dc.markers[dc.line[:-1]] = dc.row + # dc.callstack[-1].returns[dc.row] = 0 + # dc.row += 1 + # continue + # # continue + if dc.line[0]=='|' and dc.line[-1]==':': # allow override of markers in case of reuse - dc.markers[dc.line[1:]] = dc.row + frame = dc.callstack[-1] + bm = dc.line[1:-1] + dc.markers[bm] = dc.row + frame.markers[bm] = dc.row + # dc.callstack[-1].returns[dc.row] = 0 + dc.last_marker = dc.row dc.row += 1 continue @@ -463,35 +473,99 @@ 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 + # if dc.line.startswith(':'): + # jumpline = dc.line[1:] + # if dc.line.endswith("|"): + # jumpline = dc.line[1:-1] + # else: + # jumpline = dc.line[1:] + if dc.line.startswith('|||'): + dc.quitflag = True + continue + elif dc.line.startswith('||'): + if len(dc.callstack)>1: + frame = dc.callstack[-1] + frame.count = max(0,frame.count-1) + if frame.count: + dc.row = frame.row + 1 + continue + else: + dc.row = frame.caller + 1 + dc.callstack = dc.callstack[:-1] + continue else: - dc.row = dc.markers[bm] + dc.quitflag = True continue - - + if dc.line[0]==':' and dc.line[-1]=='|': + jumpline = dc.line[1:-1] + frame = dc.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 = dc.callstack[-1] + # if count: # repeats remaining + + if bm: + bmrow = dc.markers[bm] + else: + bmrow = dc.last_marker + + # if not bm: + # frame.count = max(0,frame.count-1) + # if frame.count: + # dc.row = frame.row + 1 + # continue + # else: + # dc.row += 1 + # continue + + # if bmrow in frame.returns: + + # return to marker (no pushing) + # dc.callstack.append(StackFrame(bmrow, dc.row, count)) + # dc.markers[jumpline[0]] = bmrow + # dc.row = bmrow + 1 + # dc.last_marker = bmrow + + if bmrow==dc.last_marker or bm in frame.markers: # call w/o push? + # ctx already passed bookmark, call w/o pushing (return to mark) + if dc.row in frame.returns: # have we repeated yet? + rpt = frame.returns[dc.row] + if rpt>0: + frame.returns[dc.row] = rpt - 1 + dc.row = bmrow + 1 # repeat + else: + del frame.returns[dc.row] # reset + dc.row += 1 + else: + # start return count + frame.returns[dc.row] = count - 1 + dc.row = bmrow + 1 # repeat + else: + # mark not yet passed, do push/pop + dc.callstack.append(StackFrame(bmrow, dc.row, count)) + dc.markers[bm] = bmrow + dc.row = bmrow + 1 + dc.last_marker = bmrow + + # else: + # retcount = frame.returns[dc.row] + # if retcount > count: + # dc.row = bmrow + 1 + # frame.returns[dc.row] -= 1 + # else: + # dc.row += 1 + # else: + # dc.callstack.append(StackFrame(bmrow, dc.row, count)) + # dc.markers[jumpline[0]] = bmrow + # dc.row = bmrow + 1 + # dc.last_marker = bmrow + continue + # this is not indented in blank lines because even blank lines have this logic gutter = '' if dc.shell: @@ -1299,7 +1373,9 @@ 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 + cell = [] + elif c==':': + cell = [] elif c2=='!!': # full accent vel,ct = peel_uint_s(cell[1:],127) cell = cell[2+ct:] diff --git a/src/context.py b/src/context.py index b8707a6..aea1c44 100644 --- a/src/context.py +++ b/src/context.py @@ -1,10 +1,14 @@ from . import * class StackFrame: - def __init__(self, row): + def __init__(self, row, caller, count): self.row = row - self.counter = 0 # repeat call counter - + self.caller = caller + self.count = count # repeat call counter + self.markers = {} # marker name -> line + self.returns = {} # repeat row -> number of rpts left + # self.returns[row] = 0 + class Context: def __init__(self): @@ -29,7 +33,9 @@ def __init__(self): self.ring = False # disables midi muting on program exit self.buf = [] self.markers = {} - self.callstack = [StackFrame(-1)] + f = StackFrame(-1,-1,0) + f.returns[''] = 0 + self.callstack = [f] self.schedule = [] self.separators = [] self.track_history = ['.'] * NUM_TRACKS @@ -52,6 +58,7 @@ def __init__(self): self.instrument = None self.t = 0.0 self.last_follow = 0 + self.last_marker = -1 def follow(self): if self.startrow==-1 and self.canfollow: diff --git a/src/track.py b/src/track.py index 51bb204..d82a2dc 100644 --- a/src/track.py +++ b/src/track.py @@ -54,9 +54,18 @@ def reset(self): self.flags = set() self.enabled = True self.soloed = False + self.volval = 1.0 # def _lazychannelfunc(self): # # get active channel numbers # return list(map(filter(lambda x: self.channels & x[0], [(1<0: - ch.cc(1,0) + self.refresh() self.modval = False def panic(self): for ch in self.channels: @@ -82,7 +91,7 @@ def panic(self): 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.refresh() self.modval = False def note_on(self, n, v=-1, sustain=False): if self.use_sustain_pedal: @@ -171,6 +180,7 @@ def cc(self, cc, val): # control change if cc==1: self.modval = val def mod(self, val): + self.modval = 0 return self.cc(1,val) def patch(self, p, stackidx=0): if isinstance(p,basestring): diff --git a/test/1.dc b/test/1.dc new file mode 100644 index 0000000..26ab483 --- /dev/null +++ b/test/1.dc @@ -0,0 +1,22 @@ +%t=100x4 p=piano,piano,piano c=20,-2 + +maj#4__ 1,2 + 4 + 5 + 1 + + 1 + 4 + 5 + 1 + +maj#4/b7 b7 + 4 + 5 + b7, + + b7 + 4 + 5 + b7, + diff --git a/test/2.dc b/test/2.dc new file mode 100644 index 0000000..8af6b8c --- /dev/null +++ b/test/2.dc @@ -0,0 +1,37 @@ +%t100 x1 p=piano,piano c16,-2 +ma#4/1$%v4__ 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 +3 3,2 +5mu7 +5mu7 +5mu7 + +3 +5mu7 +5mu7 +5mu7 + +3 3,1 +5mu7 +5mu7 +5mu7 + +3 +5mu7 +5mu7 +5mu7 diff --git a/test/a.dc b/test/a.dc new file mode 100644 index 0000000..bd7872b --- /dev/null +++ b/test/a.dc @@ -0,0 +1,25 @@ +%t=120x4 p=piano,bass,drums c=20,-2 + +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 + diff --git a/test/markers.dc b/test/markers.dc index 2d6d2b9..8ab51a3 100644 --- a/test/markers.dc +++ b/test/markers.dc @@ -1,5 +1,18 @@ -C -:a -D -@a -E +; should play 1 2 1 2 3 4 4 5 6 6 7 8 +1 +2 +:| +3 +:a*2| +5 +:next| +8 +||| +|a: +4 +|| +|next: +6 +:2| +7 +|| diff --git a/test/new.dc b/test/new.dc new file mode 100644 index 0000000..c8a3c4b --- /dev/null +++ b/test/new.dc @@ -0,0 +1,27 @@ +%t=120x4 p=bass,drums c=20,-2 + +1& +? +? +! +? +! +? +? +! +? +! +? +m& +? +? +! +? +! +? +? +! +? +! +? + From 85fd599d33f3cdc78886a91ee891ffa76634f183 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Thu, 28 Jun 2018 20:30:18 -0700 Subject: [PATCH 13/59] new interval notation and added back in pow chords --- decadence.py | 7 ++++--- def/default.yaml | 21 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/decadence.py b/decadence.py index df9fcea..ee52163 100755 --- a/decadence.py +++ b/decadence.py @@ -830,9 +830,10 @@ # 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:] + # 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) diff --git a/def/default.yaml b/def/default.yaml index 9bf58f7..5e69665 100644 --- a/def/default.yaml +++ b/def/default.yaml @@ -102,7 +102,7 @@ chords: p7: '7' b7: 'b7' #6: '#6' - #8: '#8' + '8': '8' #9: '#9' # '#11: '#11' # easy confusion with 11 chord @@ -191,7 +191,26 @@ chords: sus: '4 5' sus2: '2 5' + pow: '5 8' + chord_alts: + ':#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' r: '1' M2: '2' M3: '3' From e2f36f2fa3823f761da12ea1bcab3370508d59b4 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Thu, 28 Jun 2018 22:02:11 -0700 Subject: [PATCH 14/59] updated test songs, moved interval defs --- def/default.yaml | 52 ++++++++++++++++++++++-------------------------- test/columns.dc | 7 ------- test/markers.dc | 2 +- test/run.dc | 26 ++++++++++++++++++++++++ test/strum.dc | 8 ++++---- test/sus.dc | 22 ++++++++++---------- 6 files changed, 66 insertions(+), 51 deletions(-) delete mode 100644 test/columns.dc create mode 100644 test/run.dc diff --git a/def/default.yaml b/def/default.yaml index 5e69665..f1fcbf3 100644 --- a/def/default.yaml +++ b/def/default.yaml @@ -84,24 +84,37 @@ 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' #9: '#9' # '#11: '#11' # easy confusion with 11 chord @@ -194,26 +207,9 @@ chords: pow: '5 8' chord_alts: - ':#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' r: '1' - M2: '2' - M3: '3' + #M2: '2' + #M3: '3' aug: + aug7: '7+' ma#5: + 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/markers.dc b/test/markers.dc index 8ab51a3..c1fe0a2 100644 --- a/test/markers.dc +++ b/test/markers.dc @@ -1,4 +1,4 @@ -; should play 1 2 1 2 3 4 4 5 6 6 7 8 +; should play 1 2 1 2 3 4 4 5 6 6 6 7 8 1 2 :| diff --git a/test/run.dc b/test/run.dc new file mode 100644 index 0000000..547ecfd --- /dev/null +++ b/test/run.dc @@ -0,0 +1,26 @@ +%t120x2 p=piano,piano + +sus2$__ +">> +">>>> +">> +" +"<< +"<<<< + +4sus2$__ +">>> +">>>>>> +">>> +" +"<<< +"<<<<< + +5sus2$__ +">> +">>>> +">> +" +"<< +"<<<< + diff --git a/test/strum.dc b/test/strum.dc index 902b313..d3b4f69 100644 --- a/test/strum.dc +++ b/test/strum.dc @@ -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 index a0e9b8c..a95c902 100644 --- a/test/sus.dc +++ b/test/sus.dc @@ -1,19 +1,19 @@ -1 1<1 1<2 -4 +1 1,1 1,2 +3 5 1 -4 +3 5 +maj& 1 1 + + + + + 1 1 1 -4 +3 5 1 -4 -5 -1 1 1 -4 -5 -1 -4 +3 5 1' From 5e1dc3bd897008b7f547d7351c448afe6cb2aa26 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Fri, 29 Jun 2018 00:14:30 -0700 Subject: [PATCH 15/59] added cc.yaml to be used later, some fx changes --- decadence.py | 122 +++++++++++++++++++++++++++++++-------------------- def/cc.yaml | 38 ++++++++++++++++ test/2.dc | 2 +- 3 files changed, 114 insertions(+), 48 deletions(-) create mode 100644 def/cc.yaml diff --git a/decadence.py b/decadence.py index ee52163..783279f 100755 --- a/decadence.py +++ b/decadence.py @@ -418,11 +418,8 @@ elif var=='F': # flags for i in range(len(vals)): dc.tracks[i].add_flags(val.split(',')) - elif var=='K': - dc.transpose = int(val) - # for ch in TRACKS: - # ch.transpose = int(val) - elif var=='R' or var=='S': + elif var=='R' or var=='K' or var=='S': + # var R=relative usage deprecated try: if val: val = val.lower() @@ -450,7 +447,7 @@ dc.transpose = 0 # log(dc.mode-1) - if var=='R': + if var=='R' or var=='K': for i in range(dc.mode-1): inc = 0 try: @@ -652,9 +649,18 @@ 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.stop() + ch.panic() + cell_idx += 1 + continue + if cell=='==': + ch.panic() + ch.sustain = False cell_idx += 1 continue @@ -1168,6 +1174,11 @@ # cell = cell[quote+1:] # ignore = True + atsign = False + if cell and cell[0] == '@': + atsign = True + cell = cell[1:] + notevalue = '' while len(cell) >= 1: # recompute len before check after = [] # after events @@ -1253,33 +1264,37 @@ vel = min(127,int(curv + 0.5*(127.0-curv))) cell = cell[ct+1:] ch.pitch(vel) - elif c == '~': # pitch wheel - ch.pitch(127) + elif c == '~': # vibrato + ch.mod(127) # TODO: pitch osc in the future cell = cell[1:] elif c == '`': # mod wheel ch.mod(127) cell = cell[1:] # dc.sustain elif cell.startswith('--'): - ch.stop() + num, ct = count_seq('-') sustain = ch.sustain = False - cell = cell[2:] - elif cell.startswith('=='): - ch.panic() - sustain = ch.sustain = False - cell = cell[2:] + cell = cell[ct:] + 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:] elif c2=='__': sustain = ch.sustain = True cell = cell[2:] - elif c2=='_-': + elif c2=='_-': # deprecated sustain = False cell = cell[2:] elif c=='_': sustain = True cell = cell[1:] - elif cell.startswith('%v'): # volume - pass - cell = cell[2:] + elif cl=='v': # volume + cell = cell[1:] # get number num = '' for char in cell: @@ -1291,21 +1306,32 @@ 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 + elif cell.startswith('Q'): # record sequence + cell = cell[1:] + r,ct = peel_uint(cell) + # ch.record(r) + cell = cell[ct:] + elif cell.startswith('q'): # replay sequence + cell = cell[1:] + r,ct = peel_uint(cell) + # ch.replay(r) + cell = cell[ct:] + elif c2=='ch': # midi channel + num,ct = peel_uint(cell[1:]) + cell = cell[1+ct:] + ch.midi_channel(num) + if dc.showtext: + showtext.append('channel') + elif cl=='s': + # solo if used by itself (?) + # scale if given args + # ch.soloed = True + cell = cell[1:] + elif cl=='m': + ch.enabled = (c=='m') + ch.panic() + cell = cell[1:] + elif c=='c': # MIDI CC # get number cell = cell[1:] cc,ct = peel_int(cell) @@ -1316,19 +1342,14 @@ cell = cell[len(num):] ccval = int(num) ch.cc(cc,ccval) - elif cl>=2 and c=='pc': # program/patch change - cell = cell[2:] + elif cl=='p': # program/patch change + # bank select as other args? + cell = cell[1:] 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: @@ -1454,7 +1475,18 @@ cell = cell[1+ct:] if dc.showtext: showtext.append('arpeggio(&)') - elif c=='t': # tuplet + elif cl=='t': # tempo(@T) or tuplets(T) + # if atsign: + # # atsign = tempo + # num,ct = peel_uint(cell) + # cell = cell[ct:] + # dc.tempo = num + # if cell.startswith(':'): + # cell = cell[1:] + # num,ct = peel_uint(cell) + # cell = cell[ct:] + # else: + # tuplets if not ch.tuplets: ch.tuplets = True pow2i = 0.0 @@ -1477,10 +1509,6 @@ else: cell = cell[1:] pass - elif c=='@': - if not notes: - cell = [] - continue # ignore jump # elif c==':': # if not notes: # cell = [] diff --git a/def/cc.yaml b/def/cc.yaml new file mode 100644 index 0000000..32d6ffe --- /dev/null +++ b/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/test/2.dc b/test/2.dc index 8af6b8c..b0346fb 100644 --- a/test/2.dc +++ b/test/2.dc @@ -1,5 +1,5 @@ %t100 x1 p=piano,piano c16,-2 -ma#4/1$%v4__ 1'1 +ma#4/1$@v4__ 1'1 ma#4/1$ ma#4/1$ 7,1 ma#4/1$ From 363b603fb3e36baefa72a44863f1695302ad2bb4 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Fri, 29 Jun 2018 09:13:36 -0700 Subject: [PATCH 16/59] added cc mapped commands --- README.md | 46 ++++++++++++++++++++++++-- decadence.py | 85 ++++++++++++++++++++++++++----------------------- src/__init__.py | 18 ++++++++++- src/track.py | 8 +++-- 4 files changed, 112 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 48867b9..0b44dc9 100644 --- a/README.md +++ b/README.md @@ -555,10 +555,10 @@ Future: Repeat and region markers may be changed to '|:' and ':|' symbols. - future: will be moved from track commands to chord parser - ch: assign track to a midi channel - midi channels exceeding max value will be spanned across outputs -- pc: program assign +- p: program assign - Set program to a given number - Global var (%) p is usually prefered for string matching -- cc: control change (midi CC param) +- c: control change (midi CC param) - setting CC5 to 25 would be c5:25 - bs: bank select (not impl) - ~: vibrato and pitch wheel @@ -588,9 +588,51 @@ Future: Repeat and region markers may be changed to '|:' and ':|' symbols. - $: strum - plays the chord in a sequence, held by default - notes automatically fit into 1 grid beat +- `: mod +- at: aftertouch +- bc: breath controller +- fc: foot controller +- pt: portamento time +- v: volume +- bl: balance +- pn: pan +- ex: 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@v5ex5 Note: Fractional values specified are formated 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/decadence/blob/master/def/default.yaml). + ``` ## Scales, Modes, Chords, Voicings diff --git a/decadence.py b/decadence.py index 783279f..d3770f8 100755 --- a/decadence.py +++ b/decadence.py @@ -1173,14 +1173,14 @@ # BGPIPE.send((BGCMD.SAY,str(word))) # cell = cell[quote+1:] # ignore = True - - atsign = False - if cell and cell[0] == '@': - atsign = True - cell = cell[1:] notevalue = '' while len(cell) >= 1: # recompute len before check + atsign = False + if cell and cell[0] == '@': + atsign = True + cell = cell[1:] + after = [] # after events cl = len(cell) # All tokens here must be listed in CCHAR @@ -1267,10 +1267,9 @@ elif c == '~': # vibrato ch.mod(127) # TODO: pitch osc in the future cell = cell[1:] - elif c == '`': # mod wheel - ch.mod(127) - cell = cell[1:] - # dc.sustain + # elif c == '`': # mod wheel -- moved to CC + # ch.mod(127) + # cell = cell[1:] elif cell.startswith('--'): num, ct = count_seq('-') sustain = ch.sustain = False @@ -1293,19 +1292,19 @@ elif c=='_': sustain = True cell = cell[1:] - elif cl=='v': # volume - 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) + # elif cl=='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) elif cell.startswith('Q'): # record sequence cell = cell[1:] r,ct = peel_uint(cell) @@ -1414,12 +1413,12 @@ curv = ch.vel num,ct = peel_uint_s(cell[1:]) if ct: - vel = min(127,int(float('0.'+num)*127.0)) + vel = constrain(int(float('0.'+num)*127.0),127) else: if ch.accent_vel >= 0: vel = ch.accent_vel else: - vel = min(127,int(curv + 0.5*(127.0-curv))) + vel = constrain(int(curv + 0.5*(127.0-curv)),127) cell = cell[ct+1:] if dc.showtext: showtext.append('accent(!!)') @@ -1444,7 +1443,8 @@ sq = count_seq(cell) cell = cell[sq:] num,ct = peel_uint_s(cell,'0') - cell = cell[ct:] + if ct: + cell = cell[ct:] num = float('0.'+num) strum = 1.0 if len(notes)==1: # tremolo @@ -1475,18 +1475,7 @@ cell = cell[1+ct:] if dc.showtext: showtext.append('arpeggio(&)') - elif cl=='t': # tempo(@T) or tuplets(T) - # if atsign: - # # atsign = tempo - # num,ct = peel_uint(cell) - # cell = cell[ct:] - # dc.tempo = num - # if cell.startswith(':'): - # cell = cell[1:] - # num,ct = peel_uint(cell) - # cell = cell[ct:] - # else: - # tuplets + elif cl=='t': # tuplets if not ch.tuplets: ch.tuplets = True pow2i = 0.0 @@ -1517,9 +1506,27 @@ # ctrl line cell = [] break + elif c2 in CC: + cell = cell[2:] + num,ct = peel_uint_s(cell) + if ct: + num = float('0.'+num) + cell = cell[ct:] + else: + num = 1.0 + ch.cc(CC[c2],constrain(int(num*127.0),127)) + 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)) else: - if dc.dcmode in 'cl': - log(FG.BLUE + dc.line) + # if dc.dcmode in 'cl': + log(FG.BLUE + dc.line) indent = ' ' * (len(fullcell)-len(cell)) log(FG.RED + indent + "^ Unexpected " + cell[0] + " here") cell = [] diff --git a/src/__init__.py b/src/__init__.py index 069fea0..1779d8c 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -32,7 +32,7 @@ RANGE = 109 OCTAVE_BASE = 5 DRUM_WORDS = ['drum','drums','drumset','drumkit','percussion'] -CCHAR = ' <>=~.\'`,_&|!?*\"$(){}[]%' +CCHAR = ' <>=~.\'`,_&|!?*\"$(){}[]%@' CCHAR_START = 'T' # control chars PRINT = True @@ -73,6 +73,16 @@ def set_args(args): ARGS = args def get_args(): return ARGS +def constrain(a,n1=1,n2=0): + try: + int(a) + except ValueError: + # fix defaults for float + if n2==0: + n2 = 0.0 + if n1==1: + n1 = 1.0 + return min(max(n1,n2),max(min(n1,n2),a)) APPNAME = 'decadence' DIR = appdirs.AppDirs(APPNAME) @@ -174,6 +184,12 @@ def merge(a, b, overwrite=False, skip=None, diff=None, pth=None): if f.lower().endswith(DEF_EXT): merge(DEFS,load_def(f[:-len(DEF_EXT)])) +CC = {} +try: + CC = DEFS['cc'] +except KeyError: + pass + def get_defs(): return DEFS diff --git a/src/track.py b/src/track.py index d82a2dc..b89cc5d 100644 --- a/src/track.py +++ b/src/track.py @@ -179,6 +179,8 @@ def cc(self, cc, val): # control change self.player.write_short(status,cc,val) 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) @@ -201,13 +203,13 @@ def patch(self, p, stackidx=0): gmwords = list(filter(lambda x: w in x, gmwords)) lengw = len(gmwords) if lengw==1: - log('found') break elif lengw==0: - log('no match') + log(FG.RED + 'Patch \"'+p+'\" not found') assert False assert len(gmwords) > 0 - log(FG.GREEN + 'GM Patch: ' + FG.WHITE + gmwords[0]) + if self.ctx.shell: + 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 From 25352949df27373fae933280def49df8d024e2a7 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Fri, 29 Jun 2018 11:20:06 -0700 Subject: [PATCH 17/59] updated marker/repeat instructions --- README.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0b44dc9..4109a42 100644 --- a/README.md +++ b/README.md @@ -395,19 +395,20 @@ across the tracks. The midi names support partial case-insensitive matches. For a full list of GM names, see [def/gm.yaml](https://github.com/flipcoder/decadence/blob/master/config/gm.yaml). -## Markers +## Markers / Repeats -Still working on this feature, it might be broken - -':' sets marker and '@' loops to it. +Here are the marker/repeat commands: ``` -:markername -@markername +- |: 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 ``` -Repeat counting, callstack, etc. coming shortly. Code almost done. - ## Tuplets Very early support for this. See tuplet.dc example. @@ -482,11 +483,11 @@ Then at the bottom, there is a C bass note. ## Examples -Check out the examples in the test/ folder. Play them with decadence from the +Check out the examples/ folder. Play them with decadence from the command line: ``` -./decadence.py test/jazz.dc +./decadence.py examples/jazz.dc ``` # Advanced @@ -667,7 +668,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? From 4e85b61020cc44374275a0cb6d276fb1eddce6db Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Fri, 29 Jun 2018 11:35:53 -0700 Subject: [PATCH 18/59] upd readme, modified/reorg tests --- README.md | 39 ++++++++++++----------- decadence.py | 65 ++++++++++++++++++++++---------------- {test => examples}/1.dc | 0 {test => examples}/2.dc | 2 +- test/a.dc => examples/3.dc | 0 {test => examples}/jazz.dc | 0 {test => examples}/mary.dc | 0 test/markers.dc | 1 + test/organ.dc | 1 - test/softpiano.dc | 12 +++---- test/sync.dc | 2 +- 11 files changed, 66 insertions(+), 56 deletions(-) rename {test => examples}/1.dc (100%) rename {test => examples}/2.dc (95%) rename test/a.dc => examples/3.dc (100%) rename {test => examples}/jazz.dc (100%) rename {test => examples}/mary.dc (100%) diff --git a/README.md b/README.md index 4109a42..1f46963 100644 --- a/README.md +++ b/README.md @@ -305,16 +305,16 @@ For readability, notes can be indented to imply downbeat or grouping Usually you'll want to control velocity through accenting('!') or softening('?') or using values (!30 for 30%) -If you wish to control volume/gain directly, use %v +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 - ``` @@ -395,20 +395,6 @@ across the tracks. The midi names support partial case-insensitive matches. For a full list of GM names, see [def/gm.yaml](https://github.com/flipcoder/decadence/blob/master/config/gm.yaml). -## 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 -``` - ## Tuplets Very early support for this. See tuplet.dc example. @@ -492,6 +478,21 @@ command line: # 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 -): ``` diff --git a/decadence.py b/decadence.py index d3770f8..983afc1 100755 --- a/decadence.py +++ b/decadence.py @@ -40,6 +40,8 @@ --quiet no output --csound (STUB) enable csound --sonic-pi (STUB) enable sonic-pi + --in= (STUB) midi input devices (comma sep) + --analyze (STUB) midi input chord analyzer """ from __future__ import unicode_literals, print_function, generators from src import * @@ -1224,7 +1226,7 @@ # log(notes) if ch.arp_enabled: ch.arp_notes = ch.arp_notes[1:] + ch.arp_notes[:1] - cell = cell[1+ct:] + cell = cell[ct:] elif c == ',' or c=='\'': cell = cell[1:] sign = 1 if c=='\'' else -1 @@ -1248,21 +1250,22 @@ 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:] + elif cl>1 and c=='~': # pitch wheel + cell = cell[1:] + if cell[0]=='/' or cell[0]=='\\': + cell = cell[1:] + num,ct = peel_uint_s(cell) if ct: + num = float('0.'+num) + num *= 1.0 if c=='/' else -1.0 sign = 1 if num<0: num=num[1:] sign = -1 - vel = min(127,sign*int(float('0.'+num)*127.0)) + vel = constrain(sign*int(num*127.0),127) + cell = cell[ct:] else: - vel = min(127,int(curv + 0.5*(127.0-curv))) - cell = cell[ct+1:] + vel = min(127,int(ch.vel + 0.5*(127.0-ch.vel))) ch.pitch(vel) elif c == '~': # vibrato ch.mod(127) # TODO: pitch osc in the future @@ -1292,7 +1295,7 @@ elif c=='_': sustain = True cell = cell[1:] - # elif cl=='v': # volume - moved to CC + # elif c=='v': # volume - moved to CC # cell = cell[1:] # # get number # num = '' @@ -1321,12 +1324,12 @@ ch.midi_channel(num) if dc.showtext: showtext.append('channel') - elif cl=='s': + elif c=='s': # solo if used by itself (?) # scale if given args # ch.soloed = True cell = cell[1:] - elif cl=='m': + elif c=='m': ch.enabled = (c=='m') ch.panic() cell = cell[1:] @@ -1341,7 +1344,7 @@ cell = cell[len(num):] ccval = int(num) ch.cc(cc,ccval) - elif cl=='p': # program/patch change + elif c=='p': # program/patch change # bank select as other args? cell = cell[1:] p,ct = peel_int(cell) @@ -1352,9 +1355,14 @@ elif c=='*': dots = count_seq(cell) if notes: + notevalue = '*' * dots cell = cell[dots:] - num,ct = peel_float(cell, 1.0) - cell = cell[ct:] + 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)) @@ -1368,17 +1376,17 @@ elif c=='.': dots = count_seq(cell) if len(c)>1 and notes: - notevalue = '.' * dots 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 - 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: @@ -1398,9 +1406,10 @@ elif c==':': cell = [] elif c2=='!!': # full accent - vel,ct = peel_uint_s(cell[1:],127) - cell = cell[2+ct:] - if ct>2: + 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: @@ -1410,18 +1419,18 @@ if dc.showtext: showtext.append('accent(!!)') elif c=='!': # accent - curv = ch.vel - num,ct = peel_uint_s(cell[1:]) + 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(curv + 0.5*(127.0-curv)),127) - cell = cell[ct+1:] + vel = constrain(int(ch.vel + 0.5*(127.0-ch.vel)),127) if dc.showtext: - showtext.append('accent(!!)') + showtext.append('accent(!)') elif c2=='??': # ghost if ch.ghost_vel >= 0: vel = ch.ghost_vel # max(0,int(ch.vel*0.25)) @@ -1437,7 +1446,7 @@ vel = max(0,int(ch.vel*0.5)) cell = cell[1:] if dc.showtext: - showtext.append('soften(??)') + showtext.append('soften(?)') # elif cell.startswith('$$') or (c=='$' and lennotes==1): elif c=='$': # strum/spread/tremolo sq = count_seq(cell) @@ -1475,7 +1484,7 @@ cell = cell[1+ct:] if dc.showtext: showtext.append('arpeggio(&)') - elif cl=='t': # tuplets + elif c=='t': # tuplets if not ch.tuplets: ch.tuplets = True pow2i = 0.0 diff --git a/test/1.dc b/examples/1.dc similarity index 100% rename from test/1.dc rename to examples/1.dc diff --git a/test/2.dc b/examples/2.dc similarity index 95% rename from test/2.dc rename to examples/2.dc index b0346fb..352efad 100644 --- a/test/2.dc +++ b/examples/2.dc @@ -1,5 +1,5 @@ %t100 x1 p=piano,piano c16,-2 -ma#4/1$@v4__ 1'1 +ma#4/1$@v8__ 1'1 ma#4/1$ ma#4/1$ 7,1 ma#4/1$ diff --git a/test/a.dc b/examples/3.dc similarity index 100% rename from test/a.dc rename to examples/3.dc diff --git a/test/jazz.dc b/examples/jazz.dc similarity index 100% rename from test/jazz.dc rename to examples/jazz.dc diff --git a/test/mary.dc b/examples/mary.dc similarity index 100% rename from test/mary.dc rename to examples/mary.dc diff --git a/test/markers.dc b/test/markers.dc index c1fe0a2..3151bab 100644 --- a/test/markers.dc +++ b/test/markers.dc @@ -1,3 +1,4 @@ +; marker test ; should play 1 2 1 2 3 4 4 5 6 6 6 7 8 1 2 diff --git a/test/organ.dc b/test/organ.dc index 94cf28a..b55c069 100644 --- a/test/organ.dc +++ b/test/organ.dc @@ -2,7 +2,6 @@ %g4 %ppiano,piano,drums -a: maj9/1/1 . 1 b5 diff --git a/test/softpiano.dc b/test/softpiano.dc index 5e88ace..47440ea 100644 --- a/test/softpiano.dc +++ b/test/softpiano.dc @@ -2,19 +2,19 @@ ; maj7, 40% vel, sustain maj7!4_ -; maj7, 1st/B inversion, 40% vel, sustain -maj7b!4_, +; maj7, 1st inversion, 40% vel, sustain +maj7>!4_, maj7!7 -maj7b!4_, +maj7>!4_, -6mb,2_ +6m>,2_ /7m_-_ -6mb/6_!4-_ +6m>/6_!4-- -7m/6_!4-_ +7m/6_!4-- %g4 diff --git a/test/sync.dc b/test/sync.dc index 8fd07c6..6ae4e67 100644 --- a/test/sync.dc +++ b/test/sync.dc @@ -1,5 +1,5 @@ %t=120 g=4 -6<2 3 6<3 +6,2 3 6,3 6 - 6 b3 6 - From fe762df2b3601b015e9926688dd6dc6b3a7808aa Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Mon, 2 Jul 2018 21:48:42 -0700 Subject: [PATCH 19/59] (massive commit): reorg, examples, starting several new features --- README.md | 112 +-- decadence.py | 1432 +---------------------------------- def/dc.yaml | 12 +- def/default.yaml | 34 +- def/dev.yaml | 7 + def/informal.yaml | 11 +- examples/1.dc | 46 +- examples/2.dc | 36 +- examples/4.dc | 38 + examples/5.dc | 88 +++ examples/6.dc | 20 + examples/7.dc | 7 + examples/jazz.dc | 2 +- examples/mary.dc | 2 +- src/__init__.py | 7 +- src/analyzer.py | 3 + src/context.py | 79 -- src/player.py | 1550 ++++++++++++++++++++++++++++++++++++++ src/support.py | 8 +- src/theory.py | 35 + src/track.py | 30 +- test/run.dc | 14 +- test/tabs.dc | 28 + test/{sus.dc => walk.dc} | 0 24 files changed, 1956 insertions(+), 1645 deletions(-) create mode 100644 def/dev.yaml create mode 100644 examples/4.dc create mode 100644 examples/5.dc create mode 100644 examples/6.dc create mode 100644 examples/7.dc create mode 100644 src/analyzer.py delete mode 100644 src/context.py create mode 100644 src/player.py create mode 100644 test/tabs.dc rename test/{sus.dc => walk.dc} (100%) diff --git a/README.md b/README.md index 1f46963..6e2f955 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ 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) @@ -40,7 +39,7 @@ 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 @@ -524,6 +523,13 @@ Here are the marker/repeat 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) @@ -533,13 +539,10 @@ Here are the marker/repeat commands: - Supports midi patch numbers - General MIDI name matching - ;: comment -- :: set marker (requires name) -- @: go back to last marker, or start -- @@: pop mark, go back to last area -- @start: return to start -- @end: end song +- ;;: cell comment (not yet impl) -Future: Repeat and region markers may be changed to '|:' and ':|' symbols. +To do relative values, drop the equals sign: +%k-2 ``` ## Track commands @@ -555,14 +558,6 @@ Future: Repeat and region markers may be changed to '|:' and ':|' symbols. - future: will be moved from track commands to chord parser - <: lower inversion (repeatable) - future: will be moved from track commands to chord parser -- 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 prefered for string matching -- c: control change (midi CC param) - - setting CC5 to 25 would be c5:25 -- bs: bank select (not impl) - ~: vibrato and pitch wheel - `: mod wheel - ": repeat last cell (ignoring dots, blanks, mutes, modified repeats don't repeat) @@ -591,44 +586,55 @@ Future: Repeat and region markers may be changed to '|:' and ':|' symbols. - plays the chord in a sequence, held by default - notes automatically fit into 1 grid beat - `: mod -- at: aftertouch -- bc: breath controller -- fc: foot controller -- pt: portamento time -- v: volume -- bl: balance -- pn: pan -- ex: 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 +- 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 prefered 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@v5ex5 +Example: 1~ is fine, but 1v is not. Use 1@v You only need one to combine: 1@v5e5 Note: Fractional values specified are formated like numbers after a decimal point: Example: 3, 30, and 300 all mean 30% (read like .3, .30, etc.) @@ -683,7 +689,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) ``` @@ -703,5 +709,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/decadence.py b/decadence.py index 983afc1..0554a3b 100755 --- a/decadence.py +++ b/decadence.py @@ -8,8 +8,8 @@ decadence.py song.dc play song Usage: - decadence.py [--ring | --follow | --csound | --sonic-pi] [-eftnpsrxh] [SONGNAME] - decadence.py [+RANGE] [--ring || --follow | --csound | --sonic-pi] [-eftnpsrxh] [SONGNAME] + decadence.py [--midi= | --ring | --follow | --csound | --supercollider] [-eftnpsrxh] [SONGNAME] + decadence.py [+RANGE] [--ring || --follow | --csound | --supercollider] [-eftnpsrxh] [SONGNAME] decadence.py -c [COMMANDS ...] decadence.py -l [LINE_CONTENT ...] @@ -26,6 +26,7 @@ -l execute commands simultaenously -r --remote (STUB) remote, keep alive as daemon --ring don't mute midi on end + --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 @@ -39,9 +40,8 @@ --follow (old) print newlines every line, no output --quiet no output --csound (STUB) enable csound - --sonic-pi (STUB) enable sonic-pi - --in= (STUB) midi input devices (comma sep) - --analyze (STUB) midi input chord analyzer + --supercollider (STUB) enable supercollider + --input (STUB) midi input chord analyzer """ from __future__ import unicode_literals, print_function, generators from src import * @@ -61,13 +61,17 @@ # logging.basicConfig(filename=LOG_FN,level=logging.DEBUG) -dc = Context() +dc = Player() # class Marker: # def __init__(self,name,row): # self.name = name # self.line = row +midifn = ARGS['--midi'] +if midifn: + dc.midifile = mido.MidiFile(midifn) + for arg,val in iteritems(ARGS): if val: if arg == '--tempo': dc.tempo = float(val) @@ -154,14 +158,8 @@ for i in range(pygame.midi.get_count()): port = pygame.midi.get_device_info(i) portname = port[1].decode('utf-8') - # timidity # print(portname) - devs = [ - 'timidity port 0', - 'synth input port', - 'loopmidi' - # helm will autoconnect - ] + devs = get_defs()['dev'] if dc.portname: if portname.lower().startswith(dc.portname.lower()): dc.portname = portname @@ -181,7 +179,7 @@ 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)) + dc.tracks.append(Track(dc, i, mch)) mch += 2 if i==DRUM_CHANNEL else 1 if dc.sustain: @@ -237,1410 +235,10 @@ log('Read the manual and look at examples. Have fun!') log('') -for ch in dc.tracks: - ch.refresh() - -header = True # set this to false as we reached cell data -while not dc.quitflag: - dc.follow() - - try: - dc.line = '.' - try: - dc.line = dc.buf[dc.row] - if dc.row == dc.startrow: - dc.startrow = -1 - 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.interactive 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.interactive: - 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.transpose + 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)) - elif dc.remote: - pass - else: - assert False - - dc.buf += bufline - - 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.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.callstack[-1].returns[dc.row] = 0 - # dc.row += 1 - # continue - # # continue - if dc.line[0]=='|' and dc.line[-1]==':': - # allow override of markers in case of reuse - frame = dc.callstack[-1] - bm = dc.line[1:-1] - dc.markers[bm] = dc.row - frame.markers[bm] = dc.row - # dc.callstack[-1].returns[dc.row] = 0 - dc.last_marker = dc.row - 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 'TGNPSRMCXK': - 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=='K' or var=='S': - # var R=relative usage deprecated - 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[val[0],val[1]) - # except KeyError: - # pass - val = val.lower().replace(' ','') - - try: - modescale = MODES[val] - except KeyError: - 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' or var=='K': - for i in range(dc.mode-1): - inc = 0 - try: - inc = int(inter[i]) - except ValueError: - pass - dc.transpose += inc - elif var=='S': - pass - except ValueError: - raise NoSuchScale() - else: - dc.transpose = 0 - - except NoSuchScale: - print(FG.RED + 'No such scale.') - pass - - dc.row += 1 - continue - - # jumps - # if dc.line.startswith(':'): - # jumpline = dc.line[1:] - # if dc.line.endswith("|"): - # jumpline = dc.line[1:-1] - # else: - # jumpline = dc.line[1:] - if dc.line.startswith('|||'): - dc.quitflag = True - continue - elif dc.line.startswith('||'): - if len(dc.callstack)>1: - frame = dc.callstack[-1] - frame.count = max(0,frame.count-1) - if frame.count: - dc.row = frame.row + 1 - continue - else: - dc.row = frame.caller + 1 - dc.callstack = dc.callstack[:-1] - continue - else: - dc.quitflag = True - continue - if dc.line[0]==':' and dc.line[-1]=='|': - jumpline = dc.line[1:-1] - frame = dc.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 = dc.callstack[-1] - # if count: # repeats remaining - - if bm: - bmrow = dc.markers[bm] - else: - bmrow = dc.last_marker - - # if not bm: - # frame.count = max(0,frame.count-1) - # if frame.count: - # dc.row = frame.row + 1 - # continue - # else: - # dc.row += 1 - # continue - - # if bmrow in frame.returns: - - # return to marker (no pushing) - # dc.callstack.append(StackFrame(bmrow, dc.row, count)) - # dc.markers[jumpline[0]] = bmrow - # dc.row = bmrow + 1 - # dc.last_marker = bmrow - - if bmrow==dc.last_marker or bm in frame.markers: # call w/o push? - # ctx already passed bookmark, call w/o pushing (return to mark) - if dc.row in frame.returns: # have we repeated yet? - rpt = frame.returns[dc.row] - if rpt>0: - frame.returns[dc.row] = rpt - 1 - dc.row = bmrow + 1 # repeat - else: - del frame.returns[dc.row] # reset - dc.row += 1 - else: - # start return count - frame.returns[dc.row] = count - 1 - dc.row = bmrow + 1 # repeat - else: - # mark not yet passed, do push/pop - dc.callstack.append(StackFrame(bmrow, dc.row, count)) - dc.markers[bm] = bmrow - dc.row = bmrow + 1 - dc.last_marker = bmrow - - # else: - # retcount = frame.returns[dc.row] - # if retcount > count: - # dc.row = bmrow + 1 - # frame.returns[dc.row] -= 1 - # else: - # dc.row += 1 - # else: - # dc.callstack.append(StackFrame(bmrow, dc.row, count)) - # dc.markers[jumpline[0]] = bmrow - # dc.row = bmrow + 1 - # dc.last_marker = bmrow - 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.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 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 = [] - - # 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 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 - # 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 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 - - 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 'add' in chordname: - # print(chordname) - addtoks = chordname.split('add') - # print(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: - 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 frets: - # ch.strings = notes - # notes = [] - - 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 - stop = 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 - atsign = False - if cell and cell[0] == '@': - atsign = True - cell = cell[1:] - - 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[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 - elif cl>1 and c=='~': # pitch wheel - cell = cell[1:] - if cell[0]=='/' or cell[0]=='\\': - cell = cell[1:] - num,ct = peel_uint_s(cell) - if ct: - num = float('0.'+num) - num *= 1.0 if c=='/' else -1.0 - sign = 1 - if num<0: - num=num[1:] - sign = -1 - vel = constrain(sign*int(num*127.0),127) - cell = cell[ct:] - else: - vel = min(127,int(ch.vel + 0.5*(127.0-ch.vel))) - ch.pitch(vel) - elif c == '~': # vibrato - ch.mod(127) # TODO: pitch osc in the future - cell = cell[1:] - # elif c == '`': # mod wheel -- moved to CC - # ch.mod(127) - # cell = cell[1:] - elif cell.startswith('--'): - num, ct = count_seq('-') - sustain = ch.sustain = False - cell = cell[ct:] - 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:] - elif c2=='__': - sustain = ch.sustain = True - cell = cell[2:] - elif c2=='_-': # deprecated - sustain = False - cell = cell[2:] - elif c=='_': - 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) - elif cell.startswith('Q'): # record sequence - cell = cell[1:] - r,ct = peel_uint(cell) - # ch.record(r) - cell = cell[ct:] - elif cell.startswith('q'): # replay sequence - cell = cell[1:] - r,ct = peel_uint(cell) - # ch.replay(r) - cell = cell[ct:] - elif c2=='ch': # midi channel - num,ct = peel_uint(cell[1:]) - cell = cell[1+ct:] - ch.midi_channel(num) - if dc.showtext: - showtext.append('channel') - elif c=='s': - # solo if used by itself (?) - # scale if given args - # ch.soloed = True - cell = cell[1:] - elif c=='m': - ch.enabled = (c=='m') - ch.panic() - cell = cell[1:] - elif c=='c': # 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 c=='p': # program/patch change - # bank select as other args? - cell = cell[1:] - p,ct = peel_int(cell) - assert ct - cell = cell[len(num):] - # ch.cc(0,p) - ch.patch(p) - 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 dc.showtext: - showtext.append('duration(*)') - 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 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 = [] - elif c==':': - cell = [] - elif c2=='!!': # full 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 dc.showtext: - showtext.append('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 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.soft_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') - 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 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': # tuplets - 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 marker - elif c=='%': - # ctrl line - cell = [] - break - elif c2 in CC: - cell = cell[2:] - num,ct = peel_uint_s(cell) - if ct: - num = float('0.'+num) - cell = cell[ct:] - else: - num = 1.0 - ch.cc(CC[c2],constrain(int(num*127.0),127)) - 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)) - 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 delay>"=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 #shorthand: # wu: # replace: ma # add: '4' -# wu-: +# 'wu-': # replace: m # add: '4' -# wu+: +# 'wu+': # replace: + # add: '4' chord_alts: sq: sus24 + melo: mmu7 # melodic minor edges w/ 5 diff --git a/def/default.yaml b/def/default.yaml index f1fcbf3..2d96280 100644 --- a/def/default.yaml +++ b/def/default.yaml @@ -10,7 +10,7 @@ scales: - aeolian - locrian chromatic: - intervals: '111111111111' + intervals: '1;1111111111' wholetone: human: 'whole tone' intervals: '222222' @@ -84,7 +84,7 @@ scales: chords: '1': '' - '#1': '#1' + #'#1': '#1' ':#2': '#1' ':b2': 'b2' ':2': '2' @@ -97,10 +97,10 @@ chords: ':5': '5' ':#5': '#5' ':b6': 'b6' - ':6': '6' + #':6': '6' ':#6': '#6' ':b7': 'b7' - ':7': '7' + #':7': '7' ':8': '8' '2': '2' '#2': '#2' @@ -134,7 +134,6 @@ chords: 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' @@ -142,8 +141,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' @@ -153,7 +150,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' @@ -182,6 +178,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' @@ -203,13 +200,12 @@ chords: dim11: 'b3 b5 bb7 9 11' sus: '4 5' sus2: '2 5' - + mm7: 'b3 5 7' + mm9: 'b3 5 7 9' pow: '5 8' chord_alts: r: '1' - #M2: '2' - #M3: '3' aug: + aug7: '7+' ma#5: + @@ -218,22 +214,12 @@ chord_alts: -: m M: ma sus4: sus - # major: maj ma7: ma7 ma9: ma9 lyd: mab5 lyd7: ma7b5 plyd: ma#4 lyd5: ma#4 - # Madd9: maadd9 - # maor7: ma7 - # Mb5: mab5 - # M7: ma7 - # M7b5: ma7b5 - # min: m - # minor: m - # min7: m7 - # minor7: m7 p: pow o: dim o7: dim7 @@ -242,8 +228,4 @@ chord_alts: 9o: dim9 o11: dim11 11o: dim11 - # mma7: mm7 - # mma9: mm9 - # mma11: mm11 - # mma13: mm13 - + diff --git a/def/dev.yaml b/def/dev.yaml new file mode 100644 index 0000000..d1ec36f --- /dev/null +++ b/def/dev.yaml @@ -0,0 +1,7 @@ +dev: + - timidity port 0 + - synth input port + - loopmidi + - fluidsynth-midi + #- qjackctl + # helm will autoconnect diff --git a/def/informal.yaml b/def/informal.yaml index 56e32b6..eb63bbf 100644 --- a/def/informal.yaml +++ b/def/informal.yaml @@ -1,13 +1,16 @@ chords: - # majadd2 + # add2 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' + '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 diff --git a/examples/1.dc b/examples/1.dc index 26ab483..85ab2d1 100644 --- a/examples/1.dc +++ b/examples/1.dc @@ -1,22 +1,24 @@ -%t=100x4 p=piano,piano,piano c=20,-2 - -maj#4__ 1,2 - 4 - 5 - 1 - - 1 - 4 - 5 - 1 - -maj#4/b7 b7 - 4 - 5 - b7, - - b7 - 4 - 5 - b7, - +%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.dc b/examples/2.dc index 352efad..53f245c 100644 --- a/examples/2.dc +++ b/examples/2.dc @@ -1,5 +1,5 @@ %t100 x1 p=piano,piano c16,-2 -ma#4/1$@v8__ 1'1 +ma#4/1$@v5__ 1'1 ma#4/1$ ma#4/1$ 7,1 ma#4/1$ @@ -15,23 +15,23 @@ ma#4/1$ 2wu/2$ 2wu/2$ 2wu/2$ -%t100 x4 -3 3,2 -5mu7 -5mu7 -5mu7 +%t100 x4 k3 +1 1,2 +b3mu7 +b3mu7 +b3mu7 -3 -5mu7 -5mu7 -5mu7 +1 +b3mu7 +b3mu7 +b3mu7 -3 3,1 -5mu7 -5mu7 -5mu7 +1 1,1 +b3mu7 +b3mu7 +b3mu7 -3 -5mu7 -5mu7 -5mu7 +1 +b3mu7 +b3mu7 +b3mu7 diff --git a/examples/4.dc b/examples/4.dc new file mode 100644 index 0000000..a4766d2 --- /dev/null +++ b/examples/4.dc @@ -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.dc b/examples/5.dc new file mode 100644 index 0000000..b7abd4e --- /dev/null +++ b/examples/5.dc @@ -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.dc b/examples/6.dc new file mode 100644 index 0000000..0519788 --- /dev/null +++ b/examples/6.dc @@ -0,0 +1,20 @@ +%t120x4 c24,-2 ppiano,piano +'2__ +|: +1/mmu7/mmu7/mmu7& + + + + + + + + + + + + + + + +:| diff --git a/examples/7.dc b/examples/7.dc new file mode 100644 index 0000000..50ac1b4 --- /dev/null +++ b/examples/7.dc @@ -0,0 +1,7 @@ +%t120x2 c16,-2 ppiano,piano + + +phyrigian + + + diff --git a/examples/jazz.dc b/examples/jazz.dc index d2ba289..eb1b03a 100644 --- a/examples/jazz.dc +++ b/examples/jazz.dc @@ -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/examples/mary.dc b/examples/mary.dc index 7ab0dec..73cb463 100644 --- a/examples/mary.dc +++ b/examples/mary.dc @@ -32,4 +32,4 @@ 1' -. + diff --git a/src/__init__.py b/src/__init__.py index 1779d8c..0410d95 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -7,6 +7,7 @@ from future.utils import iteritems import yaml, colorama, appdirs from docopt import docopt +import mido import pygame, pygame.midi from multiprocessing import Process,Pipe from prompt_toolkit import prompt @@ -104,8 +105,6 @@ def def_path(): class SignalError(BaseException): pass -class NoSuchScale(BaseException): - pass class ParseError(BaseException): def __init__(self, s=''): super(BaseException,self).__init__(s) @@ -194,10 +193,10 @@ def get_defs(): return DEFS from .schedule import * -from .parser import * from .theory import * from .midi import * from .track import * -from .context import * +from .parser import * from .remote import * +from .player import * diff --git a/src/analyzer.py b/src/analyzer.py new file mode 100644 index 0000000..d658776 --- /dev/null +++ b/src/analyzer.py @@ -0,0 +1,3 @@ +import * from . + + diff --git a/src/context.py b/src/context.py deleted file mode 100644 index aea1c44..0000000 --- a/src/context.py +++ /dev/null @@ -1,79 +0,0 @@ -from . import * - -class StackFrame: - def __init__(self, row, caller, count): - self.row = row - self.caller = caller - self.count = count # repeat call counter - self.markers = {} # marker name -> line - self.returns = {} # repeat row -> number of rpts left - # self.returns[row] = 0 - -class Context: - - 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.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 = {} - f = StackFrame(-1,-1,0) - f.returns[''] = 0 - self.callstack = [f] - self.schedule = [] - self.separators = [] - self.track_history = ['.'] * NUM_TRACKS - self.fn = None - self.row = 0 - # self.rowno = [] - self.startrow = -1 - self.stoprow = -1 - self.dcmode = 'n' # n normal c command s sequence - self.schedule = Schedule(self) - 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.player = None - self.instrument = None - self.t = 0.0 - self.last_follow = 0 - self.last_marker = -1 - - 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 - # print(self.rowno[self.row]) - - 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/player.py b/src/player.py new file mode 100644 index 0000000..5035be8 --- /dev/null +++ b/src/player.py @@ -0,0 +1,1550 @@ +# TODO: This file includes code from prototype that will be reorganized into +# other modules + +from . import * + +class StackFrame: + def __init__(self, row, caller, count): + self.row = row + self.caller = caller + self.count = count # repeat call counter + self.markers = {} # marker name -> line + self.returns = {} # repeat row -> number of rpts left + # self.returns[row] = 0 + +class Player: + 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.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 = {} + f = StackFrame(-1,-1,0) + f.returns[''] = 0 + 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.dcmode = 'n' # n normal c command s sequence + self.schedule = Schedule(self) + 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.player = None + self.instrument = None + self.t = 0.0 # actual time + self.last_follow = 0 + self.last_marker = -1 + self.midifile = None + + 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 + # print(self.rowno[self.row]) + + def pause(self): + try: + for ch in self.tracks[:self.tracks_active]: + ch.release_all(True) + input(' === PAUSED === ') + except: + return False + return True + + def run(self): + for ch in self.tracks: + ch.refresh() + + self.header = True + + 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: + self.row = len(self.buf) + # done with file, finish playing some stuff + + arps_remaining = 0 + if self.interactive or self.dcmode 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.dcmode 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 + 'DC> '+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))+\ + # ')> ' + cline = 'DC> ('+str(int(self.tempo))+'bpm x'+str(int(self.grid))+' '+\ + note_name(self.transpose + self.tracks[0].transpose) + ' ' +\ + orr(self.tracks[0].scale,self.scale).mode_name(orr(self.tracks[0].mode,self.mode,-1))+\ + ')> ' + # if bufline.endswith('.dc'): + # 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) + fullline = self.line[:] + self.line = self.line.strip() + + # LINE COMMANDS + ctrl = False + cells = [] + + if self.line: + # COMMENTS (;) + if self.line[0] == ';': + 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 + + if self.line[0]=='|' and self.line[-1]==':': + # allow override of markers in case of reuse + frame = self.callstack[-1] + 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 + continue + + # TODO: global 'silent' commands (doesn't take 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 'TGXNPSRCKO': # 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('#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 + 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=='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 + for i in range(len(vals)): + 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=='R' or var=='S': + # var R=relative usage deprecated + try: + if val: + val = val.lower() + # ambigous 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: + print(FG.RED + 'No such scale.') + pass + else: assert False # no such var + else: assert False # no such op + + self.row += 1 + continue + + # jumps + # if self.line.startswith(':'): + # jumpline = self.line[1:] + # if self.line.endswith("|"): + # jumpline = self.line[1:-1] + # else: + # jumpline = self.line[1:] + 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]=='|': + jumpline = self.line[1:-1] + 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 += ['.'] * (len_cells - self.tracks_active) + del len_cells + + cell_idx = 0 + + # CELL LOGIC + for cell in cells: + + cell = cells[cell_idx] + ch = self.tracks[cell_idx] + fullcell = cell[:] + ignore = False + + # if self.instrument != ch.instrument: + # self.player.set_instrument(ch.instrument) + # self.instrument = ch.instrument + + cell = cell.strip() + if cell: + self.header = False + + 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 numberal or number + c,ct = peel_roman_s(tok) + ambiguous = 0 + for amb in ('ion','dor','dom','alt','dou','egy'): # TODO: make these auto + 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,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 not expanded: cell = cell[1:] + except ValueError: + ignore = True + else: + ignore = True # reenable 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 char in CCHAR: + break + if char == '\\': + reverse = True + break + # if char == '^': + # addhigherroot = True + # break + chordname += char + 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: + print(num) + cut += ct + cut -= 2 # remove "no" + chordname = chordname[:-2] # cut "no + nonotes.append(str(prefix)+str(num)) # ex: b5 + break + + # cut += 2 + + 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 'add' in chordname: + # print(chordname) + addtoks = chordname.split('add') + # print(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: + 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 frets: + # ch.strings = notes + # notes = [] + + 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 + stop = 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 + + # cell = self.fx(cell) + + accent = '' + notevalue = '' + while len(cell) >= 1: # recompute len before check + atsign = False + if cell and cell[0] == '@': + atsign = True + cell = cell[1:] + + 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 + # 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:] + 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 + elif cl>1 and c=='~': # pitch wheel + cell = cell[1:] + if cell[0]=='/' or cell[0]=='\\': + cell = cell[1:] + num,ct = peel_uint_s(cell) + if ct: + num = float('0.'+num) + num *= 1.0 if c=='/' else -1.0 + sign = 1 + if num<0: + num=num[1:] + sign = -1 + vel = constrain(sign*int(num*127.0),127) + cell = cell[ct:] + else: + vel = min(127,int(ch.vel + 0.5*(127.0-ch.vel))) + ch.pitch(vel) + elif c == '~': # vibrato + ch.mod(127) # TODO: pitch osc in the future + cell = cell[1:] + # elif c == '`': # mod wheel -- moved to CC + # ch.mod(127) + # cell = cell[1:] + elif cell.startswith('--'): + num, ct = count_seq('-') + sustain = ch.sustain = False + cell = cell[ct:] + 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:] + elif c2=='__': + sustain = ch.sustain = True + cell = cell[2:] + elif c2=='_-': # deprecated + sustain = False + cell = cell[2:] + elif c=='_': + 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) + elif cell.startswith('Q'): # record sequence + cell = cell[1:] + r,ct = peel_uint(cell) + # ch.record(r) + cell = cell[ct:] + elif cell.startswith('q'): # replay sequence + cell = cell[1:] + r,ct = peel_uint(cell) + # ch.replay(r) + cell = cell[ct:] + elif c2=='ch': # midi channel + num,ct = peel_uint(cell[1:]) + cell = cell[1+ct:] + ch.midi_channel(num) + if self.showtext: + showtext.append('channel') + elif c=='s': + # solo if used by itself (?) + # scale if given args + # ch.soloed = True + cell = cell[1:] + elif c=='m': + ch.enabled = (c=='m') + ch.panic() + cell = cell[1:] + elif c=='c': # 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 c=='p': # program/patch change + # bank select as other args? + cell = cell[1:] + p,ct = peel_int(cell) + assert ct + cell = cell[len(num):] + # ch.cc(0,p) + ch.patch(p) + 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(*)') + 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(.)') + 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 = [] + elif c==':': + cell = [] + elif c2=='!!': # full accent + 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(!!)') + elif c=='!': # accent + 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(!)') + elif c2=='??': # ghost + 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(??)') + elif c=='?': # soft + 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): + elif c=='$': # strum/spread/tremolo + 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 atsign: + ch.soft_vel = vel + if self.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 self.showtext: + showtext.append('arpeggio(&)') + elif c=='t': # tuplets + 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 marker + elif c=='%': + # ctrl line + cell = [] + break + 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)) + 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 + 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)) + else: + # if self.dcmode 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, 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: + self.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 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)))+")" + + 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 Recording + self.slot = None # careful, this can be 0 + self_slot_idx = 0 + self.ccs = {} # def _lazychannelfunc(self): # # get active channel numbers # return list(map(filter(lambda x: self.channels & x[0], [(1<> -">>>> +">>>> 1 5 ">> " "<< "<<<< -4sus2$__ +4dim$__ 1 ">>> -">>>>>> +">>>>>> 1 ">>> " "<<< "<<<<< -5sus2$__ +5dim$__ 1 ">> -">>>> +">>>> 1 ">> " "<< diff --git a/test/tabs.dc b/test/tabs.dc new file mode 100644 index 0000000..7b2247c --- /dev/null +++ b/test/tabs.dc @@ -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/sus.dc b/test/walk.dc similarity index 100% rename from test/sus.dc rename to test/walk.dc From 47df4e193836fed591e4cb5ab84ca0c63c52609c Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Mon, 2 Jul 2018 21:51:24 -0700 Subject: [PATCH 20/59] oops --- def/default.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/def/default.yaml b/def/default.yaml index 2d96280..89e0871 100644 --- a/def/default.yaml +++ b/def/default.yaml @@ -10,7 +10,7 @@ scales: - aeolian - locrian chromatic: - intervals: '1;1111111111' + intervals: '11111111111' wholetone: human: 'whole tone' intervals: '222222' From d245f18c36dc840b32372544344979db0677070c Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Wed, 8 Aug 2018 14:17:19 -0700 Subject: [PATCH 21/59] tuplet/arp fixes, bugfixes, def updates, starting lanes --- test/arp2.dc | 8 +++++ test/inversions.dc | 10 +++--- test/markers.dc | 14 +++++--- test/run2.dc | 57 +++++++++++++++++++++++++++++++++ test/{channels.dc => tracks.dc} | 0 test/tuplet2.dc | 32 ++++++++++++++++++ 6 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 test/arp2.dc create mode 100644 test/run2.dc rename test/{channels.dc => tracks.dc} (100%) create mode 100644 test/tuplet2.dc diff --git a/test/arp2.dc b/test/arp2.dc new file mode 100644 index 0000000..3cce97c --- /dev/null +++ b/test/arp2.dc @@ -0,0 +1,8 @@ +sus/sus&:2|-1 + + + + + +:| +maj7&: diff --git a/test/inversions.dc b/test/inversions.dc index 8f4ab50..3cd5596 100644 --- a/test/inversions.dc +++ b/test/inversions.dc @@ -1,13 +1,11 @@ %g.5 -maj7 -"b -"c -"d - maj7 "> ">> ">>> - +maj7 +"< +"<< +"<<< diff --git a/test/markers.dc b/test/markers.dc index 3151bab..8cf2afa 100644 --- a/test/markers.dc +++ b/test/markers.dc @@ -1,13 +1,19 @@ ; marker test -; should play 1 2 1 2 3 4 4 5 6 6 6 7 8 -1 +; should play 1 2 1 2 3 4 4 5 6 6 6 7 1' (1') 2' 2' 2' +;:| +;|: +;:|: +;||| 2 :| 3 :a*2| 5 -:next| -8 +:next|: +1' +:|x: +2' +:x*2| ||| |a: 4 diff --git a/test/run2.dc b/test/run2.dc new file mode 100644 index 0000000..8913c0f --- /dev/null +++ b/test/run2.dc @@ -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/channels.dc b/test/tracks.dc similarity index 100% rename from test/channels.dc rename to test/tracks.dc diff --git a/test/tuplet2.dc b/test/tuplet2.dc new file mode 100644 index 0000000..f1050f2 --- /dev/null +++ b/test/tuplet2.dc @@ -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! From 2ed2149af2a1dcfa8e88e49497a85e4d56f78241 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Wed, 8 Aug 2018 14:21:34 -0700 Subject: [PATCH 22/59] more changes from last commit --- decadence.py | 43 +++++++----- def/default.yaml | 39 ++++++++--- def/dev.yaml | 5 +- def/informal.yaml | 9 +++ src/__init__.py | 11 ++- src/midi.py | 1 + src/parser.py | 10 +-- src/player.py | 170 +++++++++++++++++++++++++++------------------- src/schedule.py | 21 +++--- src/theory.py | 3 +- src/track.py | 118 ++++++++++++++++++++++---------- test/markers.dc | 1 + 12 files changed, 280 insertions(+), 151 deletions(-) diff --git a/decadence.py b/decadence.py index 0554a3b..8ff1ca5 100755 --- a/decadence.py +++ b/decadence.py @@ -8,33 +8,33 @@ decadence.py song.dc play song Usage: - decadence.py [--midi= | --ring | --follow | --csound | --supercollider] [-eftnpsrxh] [SONGNAME] - decadence.py [+RANGE] [--ring || --follow | --csound | --supercollider] [-eftnpsrxh] [SONGNAME] + decadence.py [--dev= | --verbose | --midi= | --ring | --follow | --csound | --supercollider] [-eftnpsrxhv] [SONGNAME] + decadence.py [+RANGE] [--dev= | --midi= | --ring | --follow | --csound | --supercollider] [-eftnpsrxhv] [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 + -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 -c execute commands sequentially -l execute commands simultaenously - -r --remote (STUB) remote, keep alive as daemon + -r --remote (STUB) remote/daemon mode, keep alive --ring don't mute midi on end --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 sustain by default + --sustain start with sustain enabled --numbers use note numbers in output --notenames use note names in output - --flats prefer flats in output + --flats prefer flats in output (default) --sharps prefer sharps in output --lint (STUB) analyze file --follow (old) print newlines every line, no output @@ -79,7 +79,8 @@ 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 == '--dev': + dc.portname = val elif arg == '--vi': dc.vimode = True elif arg == '--patch': vals = val.split(',') @@ -155,21 +156,27 @@ print('No midi devices found.') sys.exit(1) dev = -1 + +# if dc.showtext: +# for i in range(pygame.midi.get_count()): +# print(pygame.midi.get_device_info(i)) + +DEVS = get_defs()['dev'] for i in range(pygame.midi.get_count()): port = pygame.midi.get_device_info(i) + # print(port) portname = port[1].decode('utf-8') - # print(portname) - devs = get_defs()['dev'] 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): + if dc.portname.lower() in portname.lower(): dc.portname = portname dev = i break + else: + 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()) diff --git a/def/default.yaml b/def/default.yaml index 89e0871..b159576 100644 --- a/def/default.yaml +++ b/def/default.yaml @@ -14,7 +14,7 @@ scales: 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,7 +67,7 @@ scales: - ultraphyrigian - hungarianminor - oriental - - ionianaug + - ionian#2#5 - locrianbb3bb7 neapolitan: intervals: '1222221' @@ -123,13 +132,13 @@ 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' @@ -198,12 +207,25 @@ 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' mm7: 'b3 5 7' mm9: 'b3 5 7 9' 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' aug: + @@ -216,10 +238,7 @@ chord_alts: sus4: sus ma7: ma7 ma9: ma9 - lyd: mab5 - lyd7: ma7b5 - plyd: ma#4 - lyd5: ma#4 + oma7: dimma7 p: pow o: dim o7: dim7 diff --git a/def/dev.yaml b/def/dev.yaml index d1ec36f..2844259 100644 --- a/def/dev.yaml +++ b/def/dev.yaml @@ -1,7 +1,10 @@ dev: + - synth input port # linux qsynth - timidity port 0 - - synth input port - loopmidi + - loopbe - fluidsynth-midi + - bassmidi driver (port a) #- qjackctl # helm will autoconnect + - microsoft midi mapper # lowest preference diff --git a/def/informal.yaml b/def/informal.yaml index eb63bbf..b2eb58d 100644 --- a/def/informal.yaml +++ b/def/informal.yaml @@ -24,3 +24,12 @@ chords: # 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/src/__init__.py b/src/__init__.py index 0410d95..76f9d38 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -8,7 +8,12 @@ import yaml, colorama, appdirs from docopt import docopt import mido -import pygame, pygame.midi +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 @@ -33,8 +38,8 @@ RANGE = 109 OCTAVE_BASE = 5 DRUM_WORDS = ['drum','drums','drumset','drumkit','percussion'] -CCHAR = ' <>=~.\'`,_&|!?*\"$(){}[]%@' -CCHAR_START = 'T' # control chars +CCHAR = ' <>=~.\'`,_&|!?*\"$(){}[]%@;' +CCHAR_START = 'TV' # control chars PRINT = True def cmp(a,b): diff --git a/src/midi.py b/src/midi.py index 93984d0..8179a85 100644 --- a/src/midi.py +++ b/src/midi.py @@ -2,6 +2,7 @@ MIDI_CC = 0B1011 MIDI_PROGRAM = 0B1100 MIDI_PITCH = 0B1110 +MIDI_SUSTAIN_PEDAL = 0B1000 GM = get_defs()['patches'] GM_LOWER = [""]*len(GM) for i in range(len(GM)): GM_LOWER[i] = GM[i].lower() diff --git a/src/parser.py b/src/parser.py index 51d6156..0de189d 100644 --- a/src/parser.py +++ b/src/parser.py @@ -3,10 +3,12 @@ def count_seq(seq, match=''): if not seq: return 0 + r = 0 if match == '': - match = seq[0] - seq = seq[1:] - r = 1 + try: + match = seq[0] + except IndexError: + return 0 for c in seq: if c != match: break @@ -14,7 +16,7 @@ def count_seq(seq, match=''): return r def peel_uint(s, d=None): - a,b = peel_uint_s(s,d) + a,b = peel_uint_s(s,str(d)) return (int(a),b) # don't cast diff --git a/src/player.py b/src/player.py index 5035be8..bb068f0 100644 --- a/src/player.py +++ b/src/player.py @@ -75,7 +75,8 @@ def pause(self): try: for ch in self.tracks[:self.tracks_active]: ch.release_all(True) - input(' === PAUSED === ') + print('') + input('PAUSED: Press ENTER to resume. Press Ctrl-C To quit.') except: return False return True @@ -169,7 +170,7 @@ def run(self): if self.line: # COMMENTS (;) - if self.line[0] == ';': + if self.line[0] == ';' and not self.line.startswith(';;'): self.row += 1 continue @@ -186,17 +187,6 @@ def run(self): self.row += 1 continue - if self.line[0]=='|' and self.line[-1]==':': - # allow override of markers in case of reuse - frame = self.callstack[-1] - 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 - continue - # TODO: global 'silent' commands (doesn't take time) if self.line.startswith('%'): self.line = self.line[1:].strip() # remove % and spaces @@ -348,13 +338,24 @@ def run(self): self.row += 1 continue - # jumps - # if self.line.startswith(':'): - # jumpline = self.line[1:] - # if self.line.endswith("|"): - # jumpline = self.line[1:-1] - # else: - # jumpline = self.line[1:] + # 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 @@ -372,8 +373,8 @@ def run(self): else: self.quitflag = True continue - if self.line[0]==':' and self.line[-1]=='|': - jumpline = self.line[1:-1] + 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] @@ -488,12 +489,13 @@ def run(self): self.tracks_active = len_cells else: # add empty cells for active tracks to the right - cells += ['.'] * (len_cells - self.tracks_active) + 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] @@ -633,7 +635,7 @@ def run(self): # try to get roman numberal or number c,ct = peel_roman_s(tok) ambiguous = 0 - for amb in ('ion','dor','dom','alt','dou','egy'): # TODO: make these auto + for amb in ('ion','dor','dim','dom','alt','dou','egy','aeo','dia','gui','bas','aug'): # TODO: make these auto ambiguous += tok.lower().startswith(amb) if ct and not ambiguous: lower = (c.lower()==c) @@ -770,6 +772,11 @@ def run(self): elif nn > 10: n -= 12 tok = tok[1:] + + if 'transpose' not in ch.flags: + # compensate so note letters are absolute + n -= self.transpose + ch.transpose + if not expanded: cell = cell[1:] except ValueError: ignore = True @@ -792,6 +799,8 @@ def run(self): 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 == '\\': @@ -927,9 +936,13 @@ def run(self): if is_chord: # assert not accidentals # accidentals with no note name? if reverse: - chord_notes = chord_notes[::-1] + ['1'] + if '1' in nonotes: + chord_notes = chord_notes[::-1] + else: + chord_notes = chord_notes[::-1] + ['1'] else: - chord_notes = ['1'] + chord_notes + if '1' not in nonotes: + chord_notes = ['1'] + chord_notes chord_notes += addnotes # TODO: sort # slashnotes[0].append(n + chord_root - 1 - slashidx*12) @@ -1020,11 +1033,11 @@ def run(self): ch.arp_stop() else: # continue arp - arpnext = ch.arp_next() - notes = [arpnext[0]] - delay = arpnext[1] - if not fzero(delay): - ignore = False + if ch.arp_next(self.shell or self.dcmode in 'lc'): + notes = [ch.arp_note] + delay = ch.arp_delay + if not fzero(delay): + ignore = False # schedule=True # if notes: @@ -1060,7 +1073,10 @@ def run(self): accent = '' notevalue = '' + tuplets = False while len(cell) >= 1: # recompute len before check + if fullcell=='.': + break atsign = False if cell and cell[0] == '@': atsign = True @@ -1097,7 +1113,10 @@ def run(self): # p = base + (octave+shift) * 12 # INVERSION ct = 0 - if c == '>' or c=='<': + if c2==';;': + cell = [] + break + elif c == '>' or c=='<': sign = (1 if c=='>' else -1) ct = count_seq(cell) for i in range(ct): @@ -1232,8 +1251,7 @@ def run(self): cell = cell[1:] p,ct = peel_int(cell) assert ct - cell = cell[len(num):] - # ch.cc(0,p) + cell = cell[ct:] ch.patch(p) elif c=='*': dots = count_seq(cell) @@ -1353,49 +1371,55 @@ def run(self): showtext.append('strum($)') elif c=='&': count = count_seq(cell) - num,ct = peel_uint(cell[count:],0) + 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>1: arpreverse = True + if count==2: arpreverse = True if not notes: # & restarts arp (if no note) - ch.arp_enabled = True - ch.arp_count = num - ch.arp_idx = 0 + ch.arp_restart() else: arpnotes = True - - if cell.startswith(':'): - num,ct = peel_uint(cell[1:],1) - arppattern = [num] - cell = cell[1+ct:] + arppattern = [] + while True: + if not (not arppattern and cell.startswith(':')) or\ + (arppattern and cell.startswith('|')): + break + print(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(&)') elif c=='t': # tuplets + 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 - 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) + 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 + # print('denom' + str(denom)) + # print('num ' + str(num)) ch.note_spacing = denom/float(num) # ! ch.tuplet_count = int(num) - cell = cell[ct:] - else: - cell = cell[1:] - pass + ch.tuplet_offset = 0.0 # elif c==':': # if not notes: # cell = [] @@ -1455,19 +1479,20 @@ def run(self): p = base if arpnotes: - ch.arp(notes, num, sustain, arppattern, arpreverse) - arpnext = ch.arp_next() - notes = [arpnext[0]] - delay = arpnext[1] + ch.arp(notes, arpcount, sustain, arppattern, arpreverse) + arpnext = ch.arp_next(self.shell or self.dcmode in 'lc') + notes = [ch.arp_note] + delay = ch.arp_delay # if not fcmp(delay): # pass # schedule=True - if notes: + if notes and not tuplets: ch.release_all() for ev in events: self.schedule.add(ev) + events = [] delta = 0 # how much to separate notes if strum < -EPSILON: @@ -1492,6 +1517,8 @@ def run(self): # 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 @@ -1517,34 +1544,35 @@ def run(self): else: break except KeyboardInterrupt: - # log(FG.RED + traceback.format_exc()) - self.quitflag = True - break - except: - log(FG.RED + traceback.format_exc()) - if self.shell: + if self.shell or not self.pause(): self.quitflag = True break - if not self.shell and not self.pause(): + except SignalError: + if self.shell or not self.pause(): self.quitflag = True break + except: + raise + # log(FG.RED + traceback.format_exc()) + # if self.shell or self.pause(): + # self.quitflag = True + # break if self.quitflag: break except KeyboardInterrupt: - self.quitflag = True - break - except SignalError: - self.quitflag = True - break - except: - log(FG.RED + traceback.format_exc()) - if self.shell: + if self.shell or not self.pause(): self.quitflag = True break - if not self.shell and not self.pause(): + except SignalError: + if self.shell or not self.pause(): + self.quitflag = True break + except: + log(FG.RED + traceback.format_exc()) + self.quitflag = True + break self.row += 1 diff --git a/src/schedule.py b/src/schedule.py index bc199aa..a572610 100644 --- a/src/schedule.py +++ b/src/schedule.py @@ -9,7 +9,7 @@ def __init__(self, t, func, ch): class Schedule: 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 @@ -21,11 +21,14 @@ 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 # clock = time.clock() # if self.started: @@ -54,18 +57,20 @@ def logic(self, t): ev.func(0) processed += 1 - + slp = t*(1.0-self.passed) # remaining time if slp > 0.0: if self.ctx.cansleep and self.ctx.startrow == -1: time.sleep(self.ctx.speed*slp) self.passed = 0.0 self.events = self.events[processed:] - except KeyboardInterrupt as ex: - # don't replay events + except KeyboardInterrupt: + self.events = self.events[processed:] + raise + except SignalError: + self.events = self.events[processed:] + raise + except EOFError: self.events = self.events[processed:] - raise ex - except: - # log('shedule ex: ') - QUITFLAG = True + raise diff --git a/src/theory.py b/src/theory.py index 26c372c..aef591c 100644 --- a/src/theory.py +++ b/src/theory.py @@ -201,8 +201,9 @@ def note_offset(s): n += sharps flats = count_seq(s,'b') n -= flats - s = s[:sharps + flats] + s = s[sharps + flats:] if s: + s = s.lower() for names in ALIGNED_NOTE_NAMES: try: return n + names.index(s) diff --git a/src/track.py b/src/track.py index 9829bc0..5328206 100644 --- a/src/track.py +++ b/src/track.py @@ -5,29 +5,47 @@ def __init__(self, name, slot): self.name = slot self.content = [] -class Track: - FLAGS = set('auto_roman') - def __init__(self, ctx, idx, midich): +class Tuplet: + 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.player = ctx.player 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): + FLAGS = set([ + '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.players = [player] - self.player = ctx.player - self.schedule = ctx.schedule self.channels = [midich] self.midich = midich # tracks primary midi channel self.initial_channel = midich self.non_drum_channel = midich - # self.strings = [] self.reset() def reset(self): - self.notes = [0] * RANGE - self.sustain_notes = [False] * RANGE + Lane.reset(self) self.mode = 0 # 0 is NONE which inherits global mode self.scale = None - self.instrument = 0 + # 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 @@ -38,7 +56,7 @@ def reset(self): self.arp_delay = 0.0 self.arp_sustain = False self.arp_note_spacing = 1.0 - self.arp_reverse = False + # self.arp_reverse = False self.vel = 100 self.max_vel = -1 self.soft_vel = -1 @@ -50,13 +68,14 @@ def reset(self): 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.schedule.clear_channel(self) self.flags = set() self.enabled = True self.soloed = False @@ -64,10 +83,18 @@ def reset(self): self.slots = {} # slot -> Recording self.slot = None # careful, this can be 0 self_slot_idx = 0 + self.lane = None + self.lanes = [] self.ccs = {} # def _lazychannelfunc(self): # # get active channel numbers # return list(map(filter(lambda x: self.channels & x[0], [(1<0: self.cc(1,0) # self.arp_enabled = False - self.schedule.clear_channel(self) + # self.schedule.clear_channel(self) # def cut(self): def midi_channel(self, midich, stackidx=-1): if midich==DRUM_CHANNEL: # setting to drums @@ -248,54 +279,71 @@ def patch(self, p, stackidx=0): 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): + def arp(self, notes, count=0, sustain=False, pattern=[], 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 = pattern if pattern else [1] self.arp_pattern_idx = 0 - self.arp_notes_left = len(notes) * count + 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 = False def arp_stop(self): self.arp_enabled = False self.release_all() - def arp_next(self): + def arp_next(self, stop_infinite=True): stop = False assert self.arp_enabled - note = self.arp_notes[self.arp_idx] - if self.arp_notes_left != 0: - stop = True - self.arp_notes_left -= 1 - self.arp_enabled = False - if self.arp_idx+1 == len(self.arp_notes): # cycle? + # if not self.arp_enabled: + # self.arp_note = None + # return False + # print(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 - # 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) + self.arp_idx = 0 + # else: + # self.arp_idx += 1 + return bool(self.arp_note) + def arp_restart(self, count = None): + self.arp_enabled = True + 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.tuplet_offset+self.note_spacing) % 1.0 + self.tuplet_offset += self.note_spacing - 1.0 + # if self.tuplet_offset >= 1.0 - EPSILON: + # print('!!!') + # self.tuplet_offset = 0.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) + self.tuplet_stop() + # else: + # self.tuplet_stop() + # if feq(delay,1.0): + # return 0.0 + # print(delay) return delay def tuplet_stop(self): self.tuplets = False diff --git a/test/markers.dc b/test/markers.dc index 8cf2afa..4091703 100644 --- a/test/markers.dc +++ b/test/markers.dc @@ -4,6 +4,7 @@ ;|: ;:|: ;||| +1 2 :| 3 From 7bfe6ab24567a96b0cd16dba9e251f2dabfade05 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Wed, 8 Aug 2018 19:50:51 -0700 Subject: [PATCH 23/59] added loop flag, metronome example --- decadence.py | 6 +++-- examples/metronome.dc | 5 ++++ src/__init__.py | 2 ++ src/player.py | 56 ++++++++++++++++++++++++++++++++++++++++--- src/track.py | 27 ++++++++++++++++----- 5 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 examples/metronome.dc diff --git a/decadence.py b/decadence.py index 8ff1ca5..aea95b4 100755 --- a/decadence.py +++ b/decadence.py @@ -8,8 +8,8 @@ decadence.py song.dc play song Usage: - decadence.py [--dev= | --verbose | --midi= | --ring | --follow | --csound | --supercollider] [-eftnpsrxhv] [SONGNAME] - decadence.py [+RANGE] [--dev= | --midi= | --ring | --follow | --csound | --supercollider] [-eftnpsrxhv] [SONGNAME] + decadence.py [--dev= | --verbose | --midi= | --ring | --follow | --csound | --supercollider | --loop] [-eftnpsrxhv] [SONGNAME] + decadence.py [+RANGE] [--dev= | --midi= | --ring | --follow | --csound | --supercollider | --loop] [-eftnpsrxhv] [SONGNAME] decadence.py -c [COMMANDS ...] decadence.py -l [LINE_CONTENT ...] @@ -26,6 +26,7 @@ -l execute commands simultaenously -r --remote (STUB) remote/daemon mode, keep alive --ring don't mute midi on end + --loop loop song --midi= generate midi file + play from line or maker, for range use start:end -e --edit (STUB) open file in editor @@ -103,6 +104,7 @@ elif arg == '--edit': pass elif arg == '-l' and val: dc.dcmode = 'l' elif arg == '-c' and val: dc.dcmode = 'c' + elif arg == '--loop': dc.add_flags(Player.Flag.LOOP) if dc.dcmode=='l': dc.buf = ' '.join(ARGS['LINE_CONTENT']).split(';') # ; diff --git a/examples/metronome.dc b/examples/metronome.dc new file mode 100644 index 0000000..d1b9d54 --- /dev/null +++ b/examples/metronome.dc @@ -0,0 +1,5 @@ +%n=8 p=drums c10,-2 f=loop +1 +b5 +3 +b5 diff --git a/src/__init__.py b/src/__init__.py index 76f9d38..d62dd67 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -42,6 +42,8 @@ CCHAR_START = 'TV' # control chars PRINT = True +def bit(x): + return 1 << x def cmp(a,b): return bool(a>b) - bool(a 0 + else: + for e in f: + 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 def follow(self): if self.startrow==-1 and self.canfollow: @@ -100,6 +145,10 @@ def run(self): 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 @@ -196,7 +245,7 @@ def run(self): if tok[0]==' ': tok = tok[1:] var = tok[0].upper() - if var in 'TGXNPSRCKO': # global vars % + if var in 'TGXNPSRCKOF': # global vars % cmd = tok.split(' ')[0] op = cmd[1] try: @@ -277,8 +326,9 @@ def run(self): else: self.tracks[i].patch(p) elif var=='F': # flags - for i in range(len(vals)): - self.tracks[i].add_flags(val.split(',')) + 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': diff --git a/src/track.py b/src/track.py index 5328206..9baf170 100644 --- a/src/track.py +++ b/src/track.py @@ -25,10 +25,13 @@ def reset(self): self.sustain_notes = [False] * RANGE class Track(Lane): - FLAGS = set([ + class Flag: + ROMAN = bit(0) + # TRANSPOSE = bit(1) + FLAGS = [ 'roman', # STUB: fit roman chord in scale shape - 'transpose', # allow transposition of note letters - ]) + # 'transpose', # allow transposition of note letters + ] def __init__(self, ctx, idx, midich): Lane.__init__(self,ctx,idx,midich) # self.players = [player] @@ -76,7 +79,7 @@ def reset(self): 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() + self.flags = 0 # set() self.enabled = True self.soloed = False self.volval = 1.0 @@ -106,9 +109,21 @@ def refresh(self): self.volume(self.volval) self.ccs[7] = v def add_flags(self, f): - if f != f & FLAGS: - raise ParseError('invalid flags') + if isinstance(f, str): + f = 1 << FLAGS.index(f) + else: + assert f > 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 enable(self, v=True): was = v if not was and v: From 6e06c48fbb94a6c24246fb4da1e7187309b82927 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Thu, 9 Aug 2018 15:12:38 -0700 Subject: [PATCH 24/59] fix some defs, use default midi output when no matches --- decadence.py | 3 ++- def/default.yaml | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/decadence.py b/decadence.py index aea95b4..4fe5a2b 100755 --- a/decadence.py +++ b/decadence.py @@ -180,7 +180,8 @@ dev = i break -# dc.player = pygame.midi.Output(pygame.midi.get_default_output_id()) +if dev == -1: + dev = pygame.midi.get_default_output_id() dc.player = pygame.midi.Output(dev) dc.instrument = 0 diff --git a/def/default.yaml b/def/default.yaml index b159576..79757f4 100644 --- a/def/default.yaml +++ b/def/default.yaml @@ -72,7 +72,7 @@ scales: neapolitan: intervals: '1222221' modes: - - neapolitan + - neapolitanmajor - leadingwholetone - lydianaugdom - minorlydian @@ -81,7 +81,7 @@ scales: - superlocrianbb3 neapolitanminor: human: 'neapolitan minor' - intervals: '222222' + intervals: '1222131' modes: - neapolitanminor - lydian#6 From 1d0fc16b05b6bbff7c7cfbe8a64c6def9ab80867 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Thu, 9 Aug 2018 16:27:20 -0700 Subject: [PATCH 25/59] fixed old flag check --- src/player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/player.py b/src/player.py index 13b2830..dab65a0 100644 --- a/src/player.py +++ b/src/player.py @@ -823,7 +823,7 @@ def run(self): n -= 12 tok = tok[1:] - if 'transpose' not in ch.flags: + if ch.flags & Player.Flag.TRANSPOSE: # compensate so note letters are absolute n -= self.transpose + ch.transpose From 3dd84f8a25929ced00368ba5ed86508343d53331 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Sat, 11 Aug 2018 20:24:06 -0700 Subject: [PATCH 26/59] mixed outs, carla instrument rack support --- decadence.py | 84 ++++++++++++++++++++++++++++++++++++++------------ def/dev.yaml | 8 ++++- examples/3.dc | 16 +++++----- src/parser.py | 12 ++++---- src/player.py | 84 +++++++++++++++++++++++++++++++++++++------------- src/support.py | 50 +++++++++++++++++++++++++----- src/track.py | 72 +++++++++++++++++++++++++++---------------- 7 files changed, 235 insertions(+), 91 deletions(-) diff --git a/decadence.py b/decadence.py index 4fe5a2b..34423f3 100755 --- a/decadence.py +++ b/decadence.py @@ -8,8 +8,8 @@ decadence.py song.dc play song Usage: - decadence.py [--dev= | --verbose | --midi= | --ring | --follow | --csound | --supercollider | --loop] [-eftnpsrxhv] [SONGNAME] - decadence.py [+RANGE] [--dev= | --midi= | --ring | --follow | --csound | --supercollider | --loop] [-eftnpsrxhv] [SONGNAME] + decadence.py [--dev= | --verbose | --midi= | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME] + decadence.py [+RANGE] [--dev= | --midi= | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME] decadence.py -c [COMMANDS ...] decadence.py -l [LINE_CONTENT ...] @@ -40,8 +40,6 @@ --lint (STUB) analyze file --follow (old) print newlines every line, no output --quiet no output - --csound (STUB) enable csound - --supercollider (STUB) enable supercollider --input (STUB) midi input chord analyzer """ from __future__ import unicode_literals, print_function, generators @@ -105,6 +103,7 @@ elif arg == '-l' and val: dc.dcmode = 'l' elif arg == '-c' and val: dc.dcmode = 'c' elif arg == '--loop': dc.add_flags(Player.Flag.LOOP) + elif arg == '--renderman': dc.renderman = True if dc.dcmode=='l': dc.buf = ' '.join(ARGS['LINE_CONTENT']).split(';') # ; @@ -164,28 +163,68 @@ # print(pygame.midi.get_device_info(i)) DEVS = get_defs()['dev'] -for i in range(pygame.midi.get_count()): - port = pygame.midi.get_device_info(i) - # print(port) - portname = port[1].decode('utf-8') - if dc.portname: - if dc.portname.lower() in portname.lower(): - dc.portname = portname - dev = i - break - else: - for name in DEVS: +if dc.showtext: + print('MIDI Devices:') +portnames = [] +breakall = False +for name in DEVS: + firstpass = True + 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 dc.showtext: + print(' '*4 + portname) + if dc.portname: + if dc.portname.lower() in portname.lower(): + dc.portname = portname + dev = i + breakall = True + break + else: if portname.lower().startswith(name): dc.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 dc.showtext: +# print(' '*4 + portname) +# if dc.portname: +# if dc.portname.lower() in portname.lower(): +# dc.portname = portname +# dev = i +# break +# else: +# for name in DEVS: +# if portname.lower().startswith(name): +# dc.portname = portname +# dev = i +# break +# portnames += [portname] +if dc.showtext: + print('') if dev == -1: dev = pygame.midi.get_default_output_id() -dc.player = pygame.midi.Output(dev) +dc.midi += [pygame.midi.Output(dev)] dc.instrument = 0 -dc.player.set_instrument(0) +dc.midi[0].set_instrument(0) mch = 0 for i in range(NUM_CHANNELS_PER_DEVICE): # log("%s -> %s" % (i,mch)) @@ -237,10 +276,13 @@ 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',)) + log(FG.RED + 'Other Devices: ' + FG.WHITE + '%s' % (', '.join(portnames))) + if dc.portname: 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(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('') @@ -255,9 +297,11 @@ for ch in dc.tracks: if not dc.ring: ch.panic() - ch.player = None + ch.midi = None -del dc.player +for mididev in dc.midi: + del mididev +dc.midi = [] pygame.midi.quit() # def main(): diff --git a/def/dev.yaml b/def/dev.yaml index 2844259..fac3bce 100644 --- a/def/dev.yaml +++ b/def/dev.yaml @@ -4,7 +4,13 @@ dev: - loopmidi - loopbe - fluidsynth-midi + - fluid-synth - bassmidi driver (port a) - #- qjackctl # helm will autoconnect - microsoft midi mapper # lowest preference + - midi in + - hexter + - virtual raw midi # carla + - virmidi # linux virtual midi + - qjackctl + diff --git a/examples/3.dc b/examples/3.dc index bd7872b..53bc9fa 100644 --- a/examples/3.dc +++ b/examples/3.dc @@ -1,25 +1,25 @@ %t=120x4 p=piano,bass,drums c=20,-2 -M. 3 1 +ma. 3,1 1 m?. m?. -M. +ma. m?. 3 -M. +ma. m?. m?. -M. 1 +ma. 1 m?. m?. -M. b7 +ma. b7 m?. 3 m?. -M. +ma. m?. -M. 1 +ma. 1 m?. m?. -M. +ma. m?. 3 m?. 5 diff --git a/src/parser.py b/src/parser.py index 0de189d..eb58da9 100644 --- a/src/parser.py +++ b/src/parser.py @@ -16,8 +16,12 @@ def count_seq(seq, match=''): return r def peel_uint(s, d=None): - a,b = peel_uint_s(s,str(d)) - return (int(a),b) + a,b = peel_uint_s(s,str(d) if d!=None else None) + return (int(a) if a!=None and a!='' else None,b) + +def peel_int(s, d=None): + a,b = peel_uint_s(s,str(d) if d!=None else None) + return (int(a) if a!=None and a!='' else None,b) # don't cast def peel_uint_s(s, d=None): @@ -47,10 +51,6 @@ def peel_roman_s(s, d=None): if not r: return (d,0) if d!=None else ('',0) return (r,len(r)) -def peel_int(s, d=None): - a,b = peel_int_s(s,d) - return (int(a),b) - def peel_int_s(s, d=None): r = '' for ch in s: diff --git a/src/player.py b/src/player.py index dab65a0..cd23b68 100644 --- a/src/player.py +++ b/src/player.py @@ -47,7 +47,7 @@ def __init__(self): 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.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 = [] @@ -72,7 +72,7 @@ def __init__(self): self.portname = '' self.speed = 1.0 self.muted = False # mute all except for solo tracks - self.player = None + self.midi = [] self.instrument = None self.t = 0.0 # actual time self.last_follow = 0 @@ -80,6 +80,24 @@ def __init__(self): self.midifile = None self.flags = 0 + # require enable at top of file + self.devices = ['midi'] + + self.renderman = False + + def refresh_devices(self): + # determine output device support and load external programs + from . import support + for dev in self.devices: + if not support.supports(dev): + print('device not supported by system: ' + dev) + assert False + try: + support.support_init[dev]() + except KeyError: + # no init needed, silent + pass + def add_flags(self, f): if isinstance(f, basestring): f = 1 << self.FLAGS.index(f) @@ -245,7 +263,7 @@ def run(self): if tok[0]==' ': tok = tok[1:] var = tok[0].upper() - if var in 'TGXNPSRCKOF': # global vars % + if var in 'TGXNPSRCKOFD': # global vars % cmd = tok.split(' ')[0] op = cmd[1] try: @@ -301,6 +319,9 @@ def run(self): # self.transpose = self.transpose%12 elif op=='=': if var in 'GX': self.grid=float(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': @@ -1127,10 +1148,10 @@ def run(self): while len(cell) >= 1: # recompute len before check if fullcell=='.': break - atsign = False - if cell and cell[0] == '@': - atsign = True - cell = cell[1:] + spacer = False + if cell.strip() and cell[0] in '@ ': + spacer = True + cell = cell[count_seq(cell):] after = [] # after events cl = len(cell) @@ -1204,21 +1225,27 @@ def run(self): # row_events += 1 elif cl>1 and c=='~': # pitch wheel 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: + 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) - num *= 1.0 if c=='/' else -1.0 - sign = 1 - if num<0: - num=num[1:] - sign = -1 - vel = constrain(sign*int(num*127.0),127) - cell = cell[ct:] - else: - vel = min(127,int(ch.vel + 0.5*(127.0-ch.vel))) - ch.pitch(vel) + 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) elif c == '~': # vibrato ch.mod(127) # TODO: pitch osc in the future cell = cell[1:] @@ -1290,10 +1317,10 @@ def run(self): cell = cell[1:] cc,ct = peel_int(cell) assert ct - cell = cell[len(num)+1:] + cell = cell[ct+1:] ccval,ct = peel_int(cell) assert ct - cell = cell[len(num):] + cell = cell[ct:] ccval = int(num) ch.cc(cc,ccval) elif c=='p': # program/patch change @@ -1303,6 +1330,19 @@ def run(self): assert ct cell = cell[ct:] ch.patch(p) + elif c2=='bs': # program/patch change + 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) + assert ct + cell = cell[ct:] + b = num2 # second val -> lsb + b |= num << 8 # first value -> msb + ch.bank(b) elif c=='*': dots = count_seq(cell) if notes: @@ -1415,7 +1455,7 @@ def run(self): notes = notes * 2 # notes = [notes[i:i + sq] for i in range(0, len(notes), sq)] # log('strum') - if atsign: + if spacer: ch.soft_vel = vel if self.showtext: showtext.append('strum($)') diff --git a/src/support.py b/src/support.py index 7d10a18..3b6d151 100644 --- a/src/support.py +++ b/src/support.py @@ -1,18 +1,52 @@ +from . import * from . import get_args +import shutil ARGS = get_args() SUPPORT = set(['midi']) -SUPPORT_ALL = set(['supercollider','csound','midi']) # gme,mpe +SUPPORT_ALL = set(['carla','supercollider','csound','midi','gme']) # gme,mpe psonic = None -if ARGS['--supercollider']: - import osc - SUPPORT.add('supercollider') +if shutil.which('carla'): + SUPPORT.add('carla') + +if shutil.which('scsynth'): + try: + import osc + SUPPORT.add('supercollider') + except: + pass 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) +if shutil.which('csound'): SUPPORT.add('csound') +def supports(dev): + global SUPPORT + return dev in SUPPORT + +csound_inited = False +def csound_init(): + global csound_inited + if not csound_inited: + import subprocess + 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) + csound_inited = True + +carla_inited = False +carla_proc = None +def carla_init(): + global carla_proc + if not carla_proc: + fn = ARGS['SONGNAME'] + if not fn: + fn = 'default' + carla_proc = subprocess.Popen(['carla', '--nogui', fn.split('.')[0]+'.carxp'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + +support_init = { + 'csound': csound_init, + 'carla': carla_init +} + def csound_send(s): assert csound return csound.sendto(s,('localhost',CSOUND_PORT)) @@ -69,6 +103,8 @@ def bgproc_run(con): def support_stop(): if csound and csound_proc: csound_proc.kill() + if carla_proc: + carla_proc.kill() if BGPROC: BGPIPE.send((BGCMD.QUIT,)) BGPROC.join() diff --git a/src/track.py b/src/track.py index 9baf170..2793500 100644 --- a/src/track.py +++ b/src/track.py @@ -15,7 +15,7 @@ def __init__(self): class Lane(object): def __init__(self, ctx, idx, midich, parent=None): self.idx = idx - self.player = ctx.player + self.midi = ctx.midi self.ctx = ctx self.schedule = self.ctx.schedule def master(self): @@ -34,8 +34,8 @@ class Flag: ] def __init__(self, ctx, idx, midich): Lane.__init__(self,ctx,idx,midich) - # self.players = [player] - self.channels = [midich] + # self.midis = [player] + self.channels = [(0,midich)] self.midich = midich # tracks primary midi channel self.initial_channel = midich self.non_drum_channel = midich @@ -89,9 +89,18 @@ def reset(self): 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: self.refresh() self.modval = False def panic(self): self.release_all(True) for ch in self.channels: - status = (MIDI_CC<<4) + ch + status = (MIDI_CC<<4) + ch[1] if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status,123,0)) - self.player.write_short(status, 123, 0) + self.midi[ch[0]].write_short(status, 123, 0) if self.modval>0: self.refresh() self.modval = False @@ -166,10 +175,10 @@ def note_on(self, n, v=-1, sustain=False): 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: - self.player.note_on(n,v,ch) + self.midi[ch[0]].note_on(n,v,ch[1]) if self.ctx.midifile: self.ctx.midifile.tracks[ch].append(mido.Message( - 'note_on',velocity=v,time=self.ctx.t,channel=ch + 'note_on',velocity=v,time=self.ctx.t,channel=ch[1] )) def note_off(self, n, v=-1): if v == -1: @@ -180,8 +189,8 @@ def note_off(self, n, v=-1): # 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_on(self.notes[n],0,ch) - self.player.note_off(self.notes[n],v,ch) + 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 self.cc(MIDI_SUSTAIN_PEDAL, True) @@ -196,8 +205,8 @@ def release_all(self, mute_sus=False, v=-1): 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_on(n,0,ch) - self.player.note_off(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)) @@ -209,18 +218,18 @@ def release_all(self, mute_sus=False, v=-1): # 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] + 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 + 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 = [midich] + self.channels = [(0,midich)] elif midich not in self.channels: self.channels.append(midich) def pitch(self, val): # [-1.0,1.0] @@ -229,16 +238,15 @@ def pitch(self, val): # [-1.0,1.0] val2 = (val>>0x7f) val = val&0x7f for ch in self.channels: - status = (MIDI_PITCH<<4) + self.midich + status = (MIDI_PITCH<<4) + ch[1] if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: PITCH (%s, %s)' % (val,val2)) - self.player.write_short(status,val,val2) - self.mod(0) + 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 + status = (MIDI_CC<<4) + ch[1] if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status, cc,val)) - self.player.write_short(status,cc,val) + self.midi[ch[0]].write_short(status,cc,val) self.ccs[cc] = v if cc==1: self.modval = val @@ -247,14 +255,14 @@ def cc(self, cc, val): # control change def mod(self, val): self.modval = 0 return self.cc(1,val) - def patch(self, p, stackidx=0): + 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) + self.midi__channel(DRUM_CHANNEL) p = 0 else: if self.midich == DRUM_CHANNEL: @@ -291,9 +299,19 @@ def patch(self, p, stackidx=0): 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) + 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): self.arp_enabled = True if reverse: From 16e9345740d689744b5b6da5d2a4c5079b4f4799 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Mon, 13 Aug 2018 05:10:55 -0700 Subject: [PATCH 27/59] trivial fixes, some reorg, refactor typos --- decadence.py | 2 +- def/dev.yaml | 7 ++-- presets/default.carxp | 37 ++++++++++++++++++++ presets/example.carxp | 63 +++++++++++++++++++++++++++++++++++ {voice => presets}/festivalrc | 0 {voice => presets}/test.xml | 0 requirements.txt | 1 + src/player.py | 2 ++ src/support.py | 33 +++++++++++++----- src/track.py | 2 +- 10 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 presets/default.carxp create mode 100644 presets/example.carxp rename {voice => presets}/festivalrc (100%) rename {voice => presets}/test.xml (100%) diff --git a/decadence.py b/decadence.py index 34423f3..9adb5e9 100755 --- a/decadence.py +++ b/decadence.py @@ -167,8 +167,8 @@ print('MIDI Devices:') portnames = [] breakall = False +firstpass = True for name in DEVS: - firstpass = True for i in range(pygame.midi.get_count()): port = pygame.midi.get_device_info(i) portname = port[1].decode('utf-8') diff --git a/def/dev.yaml b/def/dev.yaml index fac3bce..cb85d88 100644 --- a/def/dev.yaml +++ b/def/dev.yaml @@ -6,11 +6,12 @@ dev: - fluidsynth-midi - fluid-synth - bassmidi driver (port a) - # helm will autoconnect - microsoft midi mapper # lowest preference - - midi in + - zynaddsubfx - hexter + - midi in - virtual raw midi # carla - - virmidi # linux virtual midi + - midi through + - virmidi - qjackctl diff --git a/presets/default.carxp b/presets/default.carxp new file mode 100644 index 0000000..6b74caf --- /dev/null +++ b/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/presets/example.carxp b/presets/example.carxp new file mode 100644 index 0000000..a508f69 --- /dev/null +++ b/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/presets/festivalrc similarity index 100% rename from voice/festivalrc rename to presets/festivalrc diff --git a/voice/test.xml b/presets/test.xml similarity index 100% rename from voice/test.xml rename to presets/test.xml diff --git a/requirements.txt b/requirements.txt index b692760..aa981a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ appdirs pyyaml docopt future +shutilwhich diff --git a/src/player.py b/src/player.py index cd23b68..4bf1794 100644 --- a/src/player.py +++ b/src/player.py @@ -79,6 +79,7 @@ def __init__(self): self.last_marker = -1 self.midifile = None self.flags = 0 + self.version = '0' # require enable at top of file self.devices = ['midi'] @@ -319,6 +320,7 @@ def run(self): # self.transpose = self.transpose%12 elif op=='=': if var in 'GX': self.grid=float(val) + elif var=='V': self.version = val elif var=='D': self.devices = val.split(',') self.refresh_devices() diff --git a/src/support.py b/src/support.py index 3b6d151..95032b2 100644 --- a/src/support.py +++ b/src/support.py @@ -1,22 +1,23 @@ from . import * from . import get_args -import shutil +from shutilwhich import which ARGS = get_args() SUPPORT = set(['midi']) -SUPPORT_ALL = set(['carla','supercollider','csound','midi','gme']) # gme,mpe +SUPPORT_ALL = set(['auto','carla','supercollider','csound','midi','gme']) # gme,mpe psonic = None -if shutil.which('carla'): +if which('carla'): SUPPORT.add('carla') + SUPPORT.add('auto') -if shutil.which('scsynth'): +if which('scsynth'): try: - import osc + import oscpy SUPPORT.add('supercollider') except: pass csound = None -if shutil.which('csound'): +if which('csound'): SUPPORT.add('csound') def supports(dev): @@ -34,17 +35,31 @@ def csound_init(): carla_inited = False carla_proc = None -def carla_init(): +def carla_init(auto=False): global carla_proc if not carla_proc: + import oscpy fn = ARGS['SONGNAME'] if not fn: fn = 'default' - carla_proc = subprocess.Popen(['carla', '--nogui', fn.split('.')[0]+'.carxp'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if auto: + # embedded file -> /tmp/proj + # TODO: use tmp file of embedded file + proj = fn.split('.')[0]+'.carxp' # TEMP: generate + else: + proj = fn.split('.')[0]+'.carxp' + if os.path.exists(proj): + carla_proc = subprocess.Popen(['carla', '--nogui', proj], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + elif not auto: + log('To load a Carla project headless, create a \'%s\' file.' % proj) + +def auto_init(): + carla_init(True) support_init = { 'csound': csound_init, - 'carla': carla_init + 'carla': carla_init, + 'auto': auto_init, } def csound_send(s): diff --git a/src/track.py b/src/track.py index 2793500..009353f 100644 --- a/src/track.py +++ b/src/track.py @@ -262,7 +262,7 @@ def patch(self, p, stackidx=-1): inst = p.replace('_',' ').replace('.',' ').lower() if p in DRUM_WORDS: - self.midi__channel(DRUM_CHANNEL) + self.midi_channel(DRUM_CHANNEL) p = 0 else: if self.midich == DRUM_CHANNEL: From 382244e08f0673a5eede83a8a9dc5e7661f40cfd Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Fri, 17 Aug 2018 03:39:18 -0700 Subject: [PATCH 28/59] new name, extension, other changes, hope nothing breaks :) --- README.md | 28 ++-- def/exp.yaml | 35 ++++ examples/{1.dc => 1.txbt} | 0 examples/{2.dc => 2.txbt} | 0 examples/{3.dc => 3.txbt} | 0 examples/{4.dc => 4.txbt} | 0 examples/{5.dc => 5.txbt} | 0 examples/{6.dc => 6.txbt} | 0 examples/{7.dc => 7.txbt} | 0 examples/{jazz.dc => jazz.txbt} | 0 examples/{mary.dc => mary.txbt} | 0 examples/{metronome.dc => metronome.txbt} | 0 icon.png | Bin 548 -> 0 bytes src/__init__.py | 4 +- src/player.py | 100 ++++++++++-- src/schedule.py | 3 + src/support.py | 21 +-- src/track.py | 6 +- test/{arp.dc => arp.txbt} | 0 test/{arp2.dc => arp2.txbt} | 0 test/auto.txbt | 3 + test/cc.txbt | 7 + test/files.txbt | 6 + test/{inversions.dc => inversions.txbt} | 0 test/{markers.dc => markers.txbt} | 0 test/{modes.dc => modes.txbt} | 0 test/{new.dc => new.txbt} | 0 test/{octave.dc => octave.txbt} | 0 test/{organ.dc => organ.txbt} | 0 test/{piano.dc => piano.txbt} | 0 test/{run.dc => run.txbt} | 0 test/{run2.dc => run2.txbt} | 0 test/{scale.dc => scale.txbt} | 0 test/{softpiano.dc => softpiano.txbt} | 0 test/{strum.dc => strum.txbt} | 0 test/{sync.dc => sync.txbt} | 0 test/{tabs.dc => tabs.txbt} | 0 test/{tempo.dc => tempo.txbt} | 0 test/{tracks.dc => tracks.txbt} | 0 test/{tuplet.dc => tuplet.txbt} | 0 test/{tuplet2.dc => tuplet2.txbt} | 0 test/{walk.dc => walk.txbt} | 0 decadence.py => textbeat.py | 189 ++++++++++------------ 43 files changed, 255 insertions(+), 147 deletions(-) create mode 100644 def/exp.yaml rename examples/{1.dc => 1.txbt} (100%) rename examples/{2.dc => 2.txbt} (100%) rename examples/{3.dc => 3.txbt} (100%) rename examples/{4.dc => 4.txbt} (100%) rename examples/{5.dc => 5.txbt} (100%) rename examples/{6.dc => 6.txbt} (100%) rename examples/{7.dc => 7.txbt} (100%) rename examples/{jazz.dc => jazz.txbt} (100%) rename examples/{mary.dc => mary.txbt} (100%) rename examples/{metronome.dc => metronome.txbt} (100%) delete mode 100644 icon.png rename test/{arp.dc => arp.txbt} (100%) rename test/{arp2.dc => arp2.txbt} (100%) create mode 100644 test/auto.txbt create mode 100644 test/cc.txbt create mode 100644 test/files.txbt rename test/{inversions.dc => inversions.txbt} (100%) rename test/{markers.dc => markers.txbt} (100%) rename test/{modes.dc => modes.txbt} (100%) rename test/{new.dc => new.txbt} (100%) rename test/{octave.dc => octave.txbt} (100%) rename test/{organ.dc => organ.txbt} (100%) rename test/{piano.dc => piano.txbt} (100%) rename test/{run.dc => run.txbt} (100%) rename test/{run2.dc => run2.txbt} (100%) rename test/{scale.dc => scale.txbt} (100%) rename test/{softpiano.dc => softpiano.txbt} (100%) rename test/{strum.dc => strum.txbt} (100%) rename test/{sync.dc => sync.txbt} (100%) rename test/{tabs.dc => tabs.txbt} (100%) rename test/{tempo.dc => tempo.txbt} (100%) rename test/{tracks.dc => tracks.txbt} (100%) rename test/{tuplet.dc => tuplet.txbt} (100%) rename test/{tuplet2.dc => tuplet2.txbt} (100%) rename test/{walk.dc => walk.txbt} (100%) rename decadence.py => textbeat.py (60%) diff --git a/README.md b/README.md index 6e2f955..2d03ee5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ![Decadence](icon.png) decadence +# textbeat Plaintext music sequencer and interactive shell. @@ -10,8 +10,8 @@ Open-source under MIT License (see LICENSE file for information) Copyright (c) 2018 Grady O'Connell -- [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.** @@ -24,7 +24,7 @@ but with syntax inspired by jazz/music theory. # Features -Decadence is a new project, but you can already do lots of cool things: +Textbeat is a new project, but you can already do lots of cool things: - Strumming - Arpeggiation @@ -99,7 +99,7 @@ Both Tempo and Grid can be decimal numbers as well. 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, decadence prefers the relative/transposed note numbers +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) @@ -155,7 +155,7 @@ To control releasing of notes, use dash (-). 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 @@ -392,11 +392,11 @@ across the tracks. The midi names support partial case-insensitive matches. %t120 x2 p=piano,guitar,bass,drums c8,-2 ``` -For a full list of GM names, see [def/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/config/gm.yaml). ## Tuplets -Very early support for this. See tuplet.dc example. +Very early support for this. See tuplet example. The 't' command spreads a set of notes across a tuplet grid, starting at the first occurence of t in that group. Ratios provided will control expansion. Default is 3:4. @@ -468,11 +468,11 @@ Then at the bottom, there is a C bass note. ## Examples -Check out the examples/ folder. Play them with decadence from the +Check out the examples/ folder. Play them with textbeat from the command line: ``` -./decadence.py examples/jazz.dc +./textbeat.py examples/jazz ``` # Advanced @@ -639,7 +639,7 @@ Example: 1~ is fine, but 1v is not. Use 1@v You only need one to combine: 1@v5e5 Note: Fractional values specified are formated 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/decadence/blob/master/def/default.yaml). +CC mapping is customizable inside [def/cc.yaml](https://github.com/flipcoder/textbeat/blob/master/def/default.yaml). ``` @@ -663,9 +663,9 @@ CC mapping is customizable inside [def/cc.yaml](https://github.com/flipcoder/dec A majority of the music index is contained in inside these files: -- 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). +- Default: [def/default.yaml](https://github.com/flipcoder/textbeat/blob/master/def/default.yaml). +- Informal: [def/informal.yaml](https://github.com/flipcoder/textbeat/blob/master/def/informal.yaml). +- Experimental: [def/exp.yaml](https://github.com/flipcoder/textbeat/blob/master/def/exp.yaml). These lists does not include certain chord modifications (add, no, drop, etc.). diff --git a/def/exp.yaml b/def/exp.yaml new file mode 100644 index 0000000..8f5f9e0 --- /dev/null +++ b/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/examples/1.dc b/examples/1.txbt similarity index 100% rename from examples/1.dc rename to examples/1.txbt diff --git a/examples/2.dc b/examples/2.txbt similarity index 100% rename from examples/2.dc rename to examples/2.txbt diff --git a/examples/3.dc b/examples/3.txbt similarity index 100% rename from examples/3.dc rename to examples/3.txbt diff --git a/examples/4.dc b/examples/4.txbt similarity index 100% rename from examples/4.dc rename to examples/4.txbt diff --git a/examples/5.dc b/examples/5.txbt similarity index 100% rename from examples/5.dc rename to examples/5.txbt diff --git a/examples/6.dc b/examples/6.txbt similarity index 100% rename from examples/6.dc rename to examples/6.txbt diff --git a/examples/7.dc b/examples/7.txbt similarity index 100% rename from examples/7.dc rename to examples/7.txbt diff --git a/examples/jazz.dc b/examples/jazz.txbt similarity index 100% rename from examples/jazz.dc rename to examples/jazz.txbt diff --git a/examples/mary.dc b/examples/mary.txbt similarity index 100% rename from examples/mary.dc rename to examples/mary.txbt diff --git a/examples/metronome.dc b/examples/metronome.txbt similarity index 100% rename from examples/metronome.dc rename to examples/metronome.txbt diff --git a/icon.png b/icon.png deleted file mode 100644 index 7ef27277e2224d79efb42b06ae6914c40ee921a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 548 zcmV+<0^9wGP)%+xc^Fz3Twqvu`Wb_? zsvNwQ4@vB2DCtgPIDPICLqnW9!^?-yu=<5y6fiQfFf`6Mz#tBc`gyZvGrWHIoIy?m zVHP7JD}$-6K7)t?0|O^N4_Lw{tec@?N(RIEYgZYX4FYI*o2|5tV&2a5x mXBc(BXb6mkz-S1Jh5!J7nBd1zUZJ=E0000 '+FG.BLUE+ '('+str(int(self.tempo))+'bpm x'+str(int(self.grid))+' '+\ + # 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))+\ # ')> ' - cline = 'DC> ('+str(int(self.tempo))+'bpm x'+str(int(self.grid))+' '+\ + cline = 'txbt> ('+str(int(self.tempo))+'bpm x'+str(int(self.grid))+' '+\ note_name(self.transpose + self.tracks[0].transpose) + ' ' +\ orr(self.tracks[0].scale,self.scale).mode_name(orr(self.tracks[0].mode,self.mode,-1))+\ ')> ' - # if bufline.endswith('.dc'): + # if bufline.endswith('.txbt'): # play file? # bufline = raw_input(cline) bufline = prompt(cline, history=HISTORY, vi_mode=self.vimode) @@ -229,6 +286,19 @@ def run(self): # 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() @@ -264,7 +334,7 @@ def run(self): if tok[0]==' ': tok = tok[1:] var = tok[0].upper() - if var in 'TGXNPSRCKOFD': # global vars % + if var in 'TGXNPSRCKOFDR': # global vars % cmd = tok.split(' ')[0] op = cmd[1] try: @@ -320,6 +390,10 @@ def run(self): # 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_rack(val.split(',')) elif var=='V': self.version = val elif var=='D': self.devices = val.split(',') @@ -358,7 +432,7 @@ def run(self): self.transpose = note_offset(val) # self.octave += -1*sgn(self.transpose)*(self.transpose//12) # self.transpose = self.transpose%12 - elif var=='R' or var=='S': + elif var=='S': # var R=relative usage deprecated try: if val: @@ -1106,7 +1180,7 @@ def run(self): ch.arp_stop() else: # continue arp - if ch.arp_next(self.shell or self.dcmode in 'lc'): + if ch.arp_next(self.shell or self.cmdmode in 'lc'): notes = [ch.arp_note] delay = ch.arp_delay if not fzero(delay): @@ -1556,7 +1630,7 @@ def run(self): num = 1.0 ch.cc(CC[c],constrain(int(num*127.0),127)) else: - # if self.dcmode in 'cl': + # if self.cmdmode in 'cl': log(FG.BLUE + self.line) indent = ' ' * (len(fullcell)-len(cell)) log(FG.RED + indent + "^ Unexpected " + cell[0] + " here") @@ -1572,7 +1646,7 @@ def run(self): if arpnotes: ch.arp(notes, arpcount, sustain, arppattern, arpreverse) - arpnext = ch.arp_next(self.shell or self.dcmode in 'lc') + arpnext = ch.arp_next(self.shell or self.cmdmode in 'lc') notes = [ch.arp_note] delay = ch.arp_delay # if not fcmp(delay): diff --git a/src/schedule.py b/src/schedule.py index a572610..1f43d44 100644 --- a/src/schedule.py +++ b/src/schedule.py @@ -50,6 +50,7 @@ def logic(self, t): # sleep until next event if ev.t >= 0.0: if self.ctx.cansleep and self.ctx.startrow == -1: + self.ctx.t += self.ctx.speed*t*(ev.t-self.passed) time.sleep(self.ctx.speed*t*(ev.t-self.passed)) ev.func(0) self.passed = ev.t # only inc if positive @@ -61,8 +62,10 @@ def logic(self, t): slp = t*(1.0-self.passed) # remaining time if slp > 0.0: if self.ctx.cansleep and self.ctx.startrow == -1: + self.ctx.t += self.ctx.speed*slp time.sleep(self.ctx.speed*slp) self.passed = 0.0 + self.events = self.events[processed:] except KeyboardInterrupt: self.events = self.events[processed:] diff --git a/src/support.py b/src/support.py index 95032b2..0ae0613 100644 --- a/src/support.py +++ b/src/support.py @@ -1,13 +1,14 @@ from . import * from . import get_args from shutilwhich import which +# from xml.dom import minidom ARGS = get_args() SUPPORT = set(['midi']) -SUPPORT_ALL = set(['auto','carla','supercollider','csound','midi','gme']) # gme,mpe +SUPPORT_ALL = set(['carla','supercollider','csound','midi']) # gme,mpe psonic = None if which('carla'): SUPPORT.add('carla') - SUPPORT.add('auto') + SUPPORT.add('rack') # auto generate if which('scsynth'): try: @@ -25,7 +26,7 @@ def supports(dev): return dev in SUPPORT csound_inited = False -def csound_init(): +def csound_init(rack=[]): global csound_inited if not csound_inited: import subprocess @@ -35,31 +36,31 @@ def csound_init(): carla_inited = False carla_proc = None -def carla_init(auto=False): +def carla_init(rack): global carla_proc if not carla_proc: import oscpy fn = ARGS['SONGNAME'] if not fn: fn = 'default' - if auto: + if devs: + # generate proj file from devs # embedded file -> /tmp/proj - # TODO: use tmp file of embedded file proj = fn.split('.')[0]+'.carxp' # TEMP: generate else: proj = fn.split('.')[0]+'.carxp' if os.path.exists(proj): carla_proc = subprocess.Popen(['carla', '--nogui', proj], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - elif not auto: + elif not devs: log('To load a Carla project headless, create a \'%s\' file.' % proj) -def auto_init(): - carla_init(True) +def rack_init(rack): + carla_init(rack) support_init = { 'csound': csound_init, 'carla': carla_init, - 'auto': auto_init, + 'rack': rack_init, } def csound_send(s): diff --git a/src/track.py b/src/track.py index 009353f..ef4902f 100644 --- a/src/track.py +++ b/src/track.py @@ -177,8 +177,10 @@ def note_on(self, n, v=-1, sustain=False): and self.enabled and self.ctx.startrow==-1: self.midi[ch[0]].note_on(n,v,ch[1]) if self.ctx.midifile: - self.ctx.midifile.tracks[ch].append(mido.Message( - 'note_on',velocity=v,time=self.ctx.t,channel=ch[1] + while ch[0] >= len(self.ctx.midifile.tracks): + self.ctx.midifile.tracks.append(mido.MidiTrack()) + self.ctx.midifile.tracks[ch[0]].append(mido.Message( + 'note_on',velocity=v,time=int(self.ctx.t),channel=ch[1] )) def note_off(self, n, v=-1): if v == -1: 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.dc b/test/arp2.txbt similarity index 100% rename from test/arp2.dc rename to test/arp2.txbt 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..0ffe953 --- /dev/null +++ b/test/cc.txbt @@ -0,0 +1,7 @@ +; run this using Helm synth +%f=loop c15,-2 +1@bs0:0 +b3 +5 +b3 +:| 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 100% rename from test/inversions.dc rename to test/inversions.txbt diff --git a/test/markers.dc b/test/markers.txbt similarity index 100% rename from test/markers.dc rename to test/markers.txbt diff --git a/test/modes.dc b/test/modes.txbt similarity index 100% rename from test/modes.dc rename to test/modes.txbt diff --git a/test/new.dc b/test/new.txbt similarity index 100% rename from test/new.dc rename to test/new.txbt 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.txbt similarity index 100% rename from test/organ.dc rename to test/organ.txbt 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.dc b/test/run.txbt similarity index 100% rename from test/run.dc rename to test/run.txbt diff --git a/test/run2.dc b/test/run2.txbt similarity index 100% rename from test/run2.dc rename to test/run2.txbt 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.txbt similarity index 100% rename from test/softpiano.dc rename to test/softpiano.txbt diff --git a/test/strum.dc b/test/strum.txbt similarity index 100% rename from test/strum.dc rename to test/strum.txbt diff --git a/test/sync.dc b/test/sync.txbt similarity index 100% rename from test/sync.dc rename to test/sync.txbt diff --git a/test/tabs.dc b/test/tabs.txbt similarity index 100% rename from test/tabs.dc rename to test/tabs.txbt 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/tracks.dc b/test/tracks.txbt similarity index 100% rename from test/tracks.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.dc b/test/tuplet2.txbt similarity index 100% rename from test/tuplet2.dc rename to test/tuplet2.txbt diff --git a/test/walk.dc b/test/walk.txbt similarity index 100% rename from test/walk.dc rename to test/walk.txbt diff --git a/decadence.py b/textbeat.py similarity index 60% rename from decadence.py rename to textbeat.py index 9adb5e9..224b7eb 100755 --- a/decadence.py +++ b/textbeat.py @@ -1,17 +1,17 @@ #!/usr/bin/python -"""decadence +"""textbeat Copyright (c) 2018 Grady O'Connell Open-source under MIT License Examples: - decadence.py shell - decadence.py song.dc play song + textbeat.py shell + textbeat.py song.txbt play song Usage: - decadence.py [--dev= | --verbose | --midi= | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME] - decadence.py [+RANGE] [--dev= | --midi= | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME] - decadence.py -c [COMMANDS ...] - decadence.py -l [LINE_CONTENT ...] + textbeat.py [--dev= | --verbose | --midi= | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME] + textbeat.py [+RANGE] [--dev= | --midi= | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME] + textbeat.py -c [COMMANDS ...] + textbeat.py -l [LINE_CONTENT ...] Options: -h --help show this @@ -53,68 +53,69 @@ style = style_from_dict({ Token: '#ff0066', - Token.DC: '#00aa00', + Token.Prompt: '#00aa00', Token.Info: '#000088', }) colorama.init(autoreset=True) # logging.basicConfig(filename=LOG_FN,level=logging.DEBUG) -dc = Player() +player = Player() # class Marker: # def __init__(self,name,row): # self.name = name # self.line = row -midifn = ARGS['--midi'] -if midifn: - dc.midifile = mido.MidiFile(midifn) +midifn = None 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 + if arg == '--tempo': player.tempo = float(val) + elif arg == '--midi': + midifn = val + player.midifile = mido.MidiFile() + 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': - dc.portname = val - elif arg == '--vi': dc.vimode = True + 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(): - dc.tracks[i].patch(int(val)) + player.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.remote = True + 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) - dc.canfollow = True + player.canfollow = True 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' - elif arg == '--loop': dc.add_flags(Player.Flag.LOOP) - elif arg == '--renderman': dc.renderman = True + elif arg == '-l' and val: player.cmdmode = 'l' + elif arg == '-c' and val: player.cmdmode = 'c' + elif arg == '--loop': player.add_flags(Player.Flag.LOOP) + elif arg == '--renderman': player.renderman = True -if dc.dcmode=='l': - dc.buf = ' '.join(ARGS['LINE_CONTENT']).split(';') # ; -elif dc.dcmode=='c': - dc.buf = ' '.join(ARGS['COMMANDS']).split(' ') # spaces +if player.cmdmode=='l': + player.buf = ' '.join(ARGS['LINE_CONTENT']).split(';') # ; +elif player.cmdmode=='c': + player.buf = ' '.join(ARGS['COMMANDS']).split(' ') # spaces else: # mode n # if len(sys.argv)>=2: # FN = sys.argv[-1] if ARGS['SONGNAME']: FN = ARGS['SONGNAME'] - # dc.markers[''] = 0 # start marker + # player.markers[''] = 0 # start marker with open(FN) as f: lc = 0 for line in f.readlines(): @@ -133,24 +134,24 @@ if ls.startswith(':'): bm = ls[1:] # only store INITIAL marker positions - if not bm in dc.markers: - dc.markers[bm] = lc + 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 dc.markers: - dc.markers[bm] = lc + if not bm in player.markers: + player.markers[bm] = lc lc += 1 - dc.buf += [line] - # dc.rowno.append(lc) - dc.shell = False + player.buf += [line] + # player.rowno.append(lc) + player.shell = False else: - if dc.dcmode == 'n': - dc.dcmode = '' - dc.shell = True + if player.cmdmode == 'n': + player.cmdmode = '' + player.shell = True -dc.interactive = dc.shell or dc.remote +player.interactive = player.shell or player.remote pygame.midi.init() if pygame.midi.get_count()==0: @@ -158,12 +159,12 @@ sys.exit(1) dev = -1 -# if dc.showtext: +# if player.showtext: # for i in range(pygame.midi.get_count()): # print(pygame.midi.get_device_info(i)) DEVS = get_defs()['dev'] -if dc.showtext: +if player.showtext: print('MIDI Devices:') portnames = [] breakall = False @@ -174,17 +175,17 @@ portname = port[1].decode('utf-8') if port[3]!=1: continue - if dc.showtext: + if player.showtext: print(' '*4 + portname) - if dc.portname: - if dc.portname.lower() in portname.lower(): - dc.portname = 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): - dc.portname = portname + player.portname = portname dev = i breakall = True break @@ -202,106 +203,82 @@ # # if port[3]==1: # # continue # portname = port[1].decode('utf-8') -# if dc.showtext: +# if player.showtext: # print(' '*4 + portname) -# if dc.portname: -# if dc.portname.lower() in portname.lower(): -# dc.portname = 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): -# dc.portname = portname +# player.portname = portname # dev = i # break # portnames += [portname] -if dc.showtext: +if player.showtext: print('') if dev == -1: dev = pygame.midi.get_default_output_id() -dc.midi += [pygame.midi.Output(dev)] -dc.instrument = 0 -dc.midi[0].set_instrument(0) +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)) - dc.tracks.append(Track(dc, i, mch)) + player.tracks.append(Track(player, i, mch)) mch += 2 if i==DRUM_CHANNEL else 1 -if dc.sustain: - dc.tracks[0].sustain = dc.sustain +if player.sustain: + player.tracks[0].sustain = player.sustain # show nice output in certain modes -if dc.shell or dc.dcmode in 'cl': - dc.showtext = True +if player.shell or player.cmdmode in 'cl': + player.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.startrow = int(vals[0]) - except ValueError: - try: - dc.startrow = 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 +player.init() -if dc.shell: - log(FG.BLUE + 'decadence')# v'+str(VERSION)) +if player.shell: + log(FG.BLUE + 'textbeat')# v'+str(VERSION)) log('Copyright (c) 2018 Grady O\'Connell') - log('https://github.com/flipcoder/decadence') + log('https://github.com/flipcoder/textbeat') 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 player.portname: + log(FG.GREEN + 'Device: ' + FG.WHITE + '%s' % (player.portname if player.portname else 'Unknown',)) log(FG.RED + 'Other Devices: ' + FG.WHITE + '%s' % (', '.join(portnames))) - if dc.portname: - if dc.tracks[0].midich == DRUM_CHANNEL: + if player.portname: + if player.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(FG.GREEN + 'GM Patch: '+ FG.WHITE +'%s' % GM[player.tracks[0].patch_num]) log('Use -h for command line options.') log('Read the manual and look at examples. Have fun!') log('') -dc.run() +player.run() -if dc.midifile: - dc.save(midifn) +if player.midifile: + player.midifile.save(midifn) # TODO: turn all midi note off i = 0 -for ch in dc.tracks: - if not dc.ring: +for ch in player.tracks: + if not player.ring: ch.panic() ch.midi = None -for mididev in dc.midi: +for mididev in player.midi: del mididev -dc.midi = [] +player.midi = [] pygame.midi.quit() # def main(): From adff5e91cf9ff4f621477055c4a49dfc1bfdcd5c Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Fri, 17 Aug 2018 05:04:01 -0700 Subject: [PATCH 29/59] updated readme info, fixed key changes to notes below --- README.md | 38 +++++++++++++++++--------------------- src/player.py | 6 ++++-- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 2d03ee5..ea2dfb0 100644 --- a/README.md +++ b/README.md @@ -404,22 +404,22 @@ 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'. +For nested tripets, group those by adding an extra 'T'. Consider the 2 tracks: ``` -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 +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. ## Picking @@ -428,25 +428,21 @@ to make them line up in a default ratio of 3:4. ## Key changes -The following behavior is optional and probably not useful to many musicians. - -However, beginners may find inspiration by picking a scale instead of using -sharps and flats. +``` +# change key (this will change the key of the current scale to 3 (E)) +%k=3 -The current scale is currently accessible as a global variable (this will be per-track soon). +# to set a relative key, this will go from a major scale to relative minor scale +%k+6 -To set the notes to match scale, +# you can also go down to relative minor below +%k-6 -``` -# set notes to dorian mode (won't change key) +# scale names are supported, this changes the scale shape to dorian %s=dorian -# rotate/relative scale (will move the key note) -%r=dorian - -# in either case, you can also use mode numbers +# you can also use mode numbers %s=2 -%r=2 ``` ## Chords (Advanced) diff --git a/src/player.py b/src/player.py index e6212ea..01028dd 100644 --- a/src/player.py +++ b/src/player.py @@ -379,12 +379,14 @@ def run(self): # self.octave += -1*sgn(self.transpose)*(self.transpose//12) # self.transpose = self.transpose%12 elif op=='-': - if var=='K': self.transpose -= note_offset('#1' if val=='-' else val) + if var=='K': + self.transpose -= note_offset(val) + print(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) + # 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 From 2ea070535aebdaedba0a73697a6a65d18fa6eba2 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Thu, 23 Aug 2018 12:14:21 -0700 Subject: [PATCH 30/59] setup.py, manifest, some reorg --- .gitignore | 3 + MANIFEST.in | 2 + examples/7.txbt | 2 +- examples/metronome.txbt | 2 +- setup.py | 27 ++ src/analyzer.py | 3 - src/remote.py | 3 - test/arp.mid | Bin 0 -> 66 bytes test/cc.txbt | 1 - test/modes.txbt | 2 +- test/organ.txbt | 37 --- textbeat.py | 291 ------------------- textbeat/__main__.py | 301 ++++++++++++++++++++ textbeat/analyzer.py | 1 + {def => textbeat/def}/cc.yaml | 0 {def => textbeat/def}/dc.yaml | 0 {def => textbeat/def}/default.yaml | 0 {def => textbeat/def}/dev.yaml | 0 {def => textbeat/def}/exp.yaml | 0 {def => textbeat/def}/gm.yaml | 0 {def => textbeat/def}/informal.yaml | 0 src/__init__.py => textbeat/defs.py | 7 +- textbeat/instrument.py | 5 + {src => textbeat}/midi.py | 2 +- {src => textbeat}/parser.py | 2 +- {src => textbeat}/player.py | 80 ++++-- {presets => textbeat/presets}/default.carxp | 0 {presets => textbeat/presets}/example.carxp | 0 {presets => textbeat/presets}/festivalrc | 0 {presets => textbeat/presets}/test.xml | 0 textbeat/remote.py | 3 + textbeat/run.py | 4 + {src => textbeat}/schedule.py | 6 +- {src => textbeat}/support.py | 62 +++- {src => textbeat}/theory.py | 2 +- {src => textbeat}/track.py | 12 +- txbt | 2 + 37 files changed, 468 insertions(+), 394 deletions(-) create mode 100644 MANIFEST.in create mode 100755 setup.py delete mode 100644 src/analyzer.py delete mode 100644 src/remote.py create mode 100644 test/arp.mid delete mode 100644 test/organ.txbt delete mode 100755 textbeat.py create mode 100755 textbeat/__main__.py create mode 100644 textbeat/analyzer.py rename {def => textbeat/def}/cc.yaml (100%) rename {def => textbeat/def}/dc.yaml (100%) rename {def => textbeat/def}/default.yaml (100%) rename {def => textbeat/def}/dev.yaml (100%) rename {def => textbeat/def}/exp.yaml (100%) rename {def => textbeat/def}/gm.yaml (100%) rename {def => textbeat/def}/informal.yaml (100%) rename src/__init__.py => textbeat/defs.py (96%) create mode 100644 textbeat/instrument.py rename {src => textbeat}/midi.py (90%) rename {src => textbeat}/parser.py (99%) rename {src => textbeat}/player.py (97%) rename {presets => textbeat/presets}/default.carxp (100%) rename {presets => textbeat/presets}/example.carxp (100%) rename {presets => textbeat/presets}/festivalrc (100%) rename {presets => textbeat/presets}/test.xml (100%) create mode 100644 textbeat/remote.py create mode 100644 textbeat/run.py rename {src => textbeat}/schedule.py (97%) rename {src => textbeat}/support.py (72%) rename {src => textbeat}/theory.py (99%) rename {src => textbeat}/track.py (98%) create mode 100755 txbt diff --git a/.gitignore b/.gitignore index 7a60b85..50bc2d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ __pycache__/ +build/ +dist/ +textbeat.egg-info/ *.pyc 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/examples/7.txbt b/examples/7.txbt index 50ac1b4..be60caf 100644 --- a/examples/7.txbt +++ b/examples/7.txbt @@ -1,4 +1,4 @@ -%t120x2 c16,-2 ppiano,piano +%v0 t120x2 c16,-2 ppiano,piano phyrigian diff --git a/examples/metronome.txbt b/examples/metronome.txbt index d1b9d54..fa912ab 100644 --- a/examples/metronome.txbt +++ b/examples/metronome.txbt @@ -1,5 +1,5 @@ %n=8 p=drums c10,-2 f=loop -1 +1 b5 3 b5 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..49dd484 --- /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'], + include_package_data=True, + install_requires=[ + 'pygame','colorama','prompt_toolkit','appdirs','pyyaml','docopt','future','shutilwhich' + ], + entry_points=''' + [console_scripts] + textbeat=textbeat.__main__:main + txbt=textbeat.__main__:main + ''', + zip_safe=False +) + diff --git a/src/analyzer.py b/src/analyzer.py deleted file mode 100644 index d658776..0000000 --- a/src/analyzer.py +++ /dev/null @@ -1,3 +0,0 @@ -import * from . - - 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/test/arp.mid b/test/arp.mid new file mode 100644 index 0000000000000000000000000000000000000000..c1491f503a9722bc0888c44b69da2a7c9806907e GIT binary patch literal 66 tcmeYb$w*;fU|?flWME``;2Tnu4dm%COke | --verbose | --midi= | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME] - textbeat.py [+RANGE] [--dev= | --midi= | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME] - textbeat.py -c [COMMANDS ...] - textbeat.py -l [LINE_CONTENT ...] - -Options: - -h --help show this - -v --verbose verbose - -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 - -c execute commands sequentially - -l execute commands simultaenously - -r --remote (STUB) remote/daemon mode, keep alive - --ring don't mute midi on end - --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 (old) print newlines every line, no output - --quiet no output - --input (STUB) midi input chord analyzer -""" -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.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() - 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' and val: player.cmdmode = 'l' - elif arg == '-c' and val: player.cmdmode = 'c' - 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 -else: # mode n - # if len(sys.argv)>=2: - # FN = sys.argv[-1] - if ARGS['SONGNAME']: - FN = ARGS['SONGNAME'] - # 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 - -pygame.midi.init() -if pygame.midi.get_count()==0: - print('No midi devices found.') - sys.exit(1) -dev = -1 - -# if player.showtext: -# for i in range(pygame.midi.get_count()): -# print(pygame.midi.get_device_info(i)) - -DEVS = get_defs()['dev'] -if player.showtext: - print('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: - print(' '*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: -# print(' '*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: - print('') - -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_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 player.portname: - log(FG.GREEN + 'Device: ' + FG.WHITE + '%s' % (player.portname if player.portname else 'Unknown',)) - log(FG.RED + 'Other Devices: ' + FG.WHITE + '%s' % (', '.join(portnames))) - if player.portname: - if player.tracks[0].midich == DRUM_CHANNEL: - log(FG.GREEN + 'GM Percussion') - else: - log(FG.GREEN + 'GM Patch: '+ FG.WHITE +'%s' % GM[player.tracks[0].patch_num]) - - log('Use -h for command line options.') - log('Read the manual and look at examples. Have fun!') - 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() - -# def main(): -# pass - -# if __name__=='__main__': -# curses.wrapper(main) - -support_stop() - diff --git a/textbeat/__main__.py b/textbeat/__main__.py new file mode 100755 index 0000000..250bad2 --- /dev/null +++ b/textbeat/__main__.py @@ -0,0 +1,301 @@ +#!/usr/bin/python +"""textbeat +Copyright (c) 2018 Grady O'Connell +Open-source under MIT License + +Examples: + textbeat.py shell + textbeat.py song.txbt play song + +Usage: + textbeat.py [--dev= | --verbose | --midi= | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME] + textbeat.py [+RANGE] [--dev= | --midi= | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME] + textbeat.py -c [COMMANDS ...] + textbeat.py -l [LINE_CONTENT ...] + +Options: + -h --help show this + -v --verbose verbose + -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 simultaenously + -r --remote (STUB) remote/daemon mode, keep alive + --ring don't mute midi on end + --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 (old) print newlines every line, no output + --quiet no output + --input (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__) + 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() + 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 =='--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 + else: # mode n + # if len(sys.argv)>=2: + # FN = sys.argv[-1] + if ARGS['SONGNAME']: + FN = ARGS['SONGNAME'] + # 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 + + pygame.midi.init() + if pygame.midi.get_count()==0: + print('No midi devices found.') + sys.exit(1) + dev = -1 + +# if player.showtext: +# for i in range(pygame.midi.get_count()): +# print(pygame.midi.get_device_info(i)) + + DEVS = get_defs()['dev'] + if player.showtext: + print('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: + print(' '*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: +# print(' '*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: + print('') + + 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: ' + FG.WHITE + ', '.join(active) + FG.WHITE) + if inactive: + log(FG.RED + 'Inactive Modules: ' + FG.WHITE + ', '.join(inactive)) + if player.portname: + log(FG.GREEN + 'Device: ' + FG.WHITE + '%s' % (player.portname if player.portname else 'Unknown',)) + log(FG.RED + 'Other Devices: ' + FG.WHITE + '%s' % (', '.join(portnames))) + if player.portname: + if player.tracks[0].midich == DRUM_CHANNEL: + log(FG.GREEN + 'GM Percussion') + else: + log(FG.GREEN + 'GM Patch: '+ FG.WHITE +'%s' % GM[player.tracks[0].patch_num]) + + log('Use -h for command line options.') + log('Read the manual and look at examples. Have fun!') + 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/def/cc.yaml b/textbeat/def/cc.yaml similarity index 100% rename from def/cc.yaml rename to textbeat/def/cc.yaml diff --git a/def/dc.yaml b/textbeat/def/dc.yaml similarity index 100% rename from def/dc.yaml rename to textbeat/def/dc.yaml diff --git a/def/default.yaml b/textbeat/def/default.yaml similarity index 100% rename from def/default.yaml rename to textbeat/def/default.yaml diff --git a/def/dev.yaml b/textbeat/def/dev.yaml similarity index 100% rename from def/dev.yaml rename to textbeat/def/dev.yaml diff --git a/def/exp.yaml b/textbeat/def/exp.yaml similarity index 100% rename from def/exp.yaml rename to textbeat/def/exp.yaml diff --git a/def/gm.yaml b/textbeat/def/gm.yaml similarity index 100% rename from def/gm.yaml rename to textbeat/def/gm.yaml diff --git a/def/informal.yaml b/textbeat/def/informal.yaml similarity index 100% rename from def/informal.yaml rename to textbeat/def/informal.yaml diff --git a/src/__init__.py b/textbeat/defs.py similarity index 96% rename from src/__init__.py rename to textbeat/defs.py index 1b5824d..42e1696 100644 --- a/src/__init__.py +++ b/textbeat/defs.py @@ -1,5 +1,5 @@ #!/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 @@ -97,7 +97,7 @@ def constrain(a,n1=1,n2=0): # LOG_FN = os.path.join(DIR.user_log_dir,'.log') HISTORY_FN = os.path.join(DIR.user_config_dir, 'history') HISTORY = FileHistory(HISTORY_FN) -SCRIPT_PATH = os.path.dirname(os.path.realpath(os.path.join(__file__,'..'))) +SCRIPT_PATH = os.path.dirname(os.path.join(__file__)) CFG_PATH = os.path.join(SCRIPT_PATH, 'config') DEF_PATH = os.path.join(SCRIPT_PATH, 'def') DEF_EXT = '.yaml' @@ -135,6 +135,9 @@ def set_print(b): def log(msg): if PRINT: print(msg) +def error(msg): + if PRINT: + print(msg) def load_cfg(fn): with open(os.path.join(CFG_PATH, fn+'.yaml'),'r') as y: diff --git a/textbeat/instrument.py b/textbeat/instrument.py new file mode 100644 index 0000000..d16ac16 --- /dev/null +++ b/textbeat/instrument.py @@ -0,0 +1,5 @@ + + +class Instrument(object): + + pass diff --git a/src/midi.py b/textbeat/midi.py similarity index 90% rename from src/midi.py rename to textbeat/midi.py index 8179a85..c564e6e 100644 --- a/src/midi.py +++ b/textbeat/midi.py @@ -1,4 +1,4 @@ -from . import * +from .defs import * MIDI_CC = 0B1011 MIDI_PROGRAM = 0B1100 MIDI_PITCH = 0B1110 diff --git a/src/parser.py b/textbeat/parser.py similarity index 99% rename from src/parser.py rename to textbeat/parser.py index eb58da9..05d680b 100644 --- a/src/parser.py +++ b/textbeat/parser.py @@ -1,4 +1,4 @@ -from . import * +from .defs import * def count_seq(seq, match=''): if not seq: diff --git a/src/player.py b/textbeat/player.py similarity index 97% rename from src/player.py rename to textbeat/player.py index 01028dd..948b741 100644 --- a/src/player.py +++ b/textbeat/player.py @@ -1,9 +1,9 @@ -# TODO: This file includes code from prototype that will be reorganized into +# TODO: This file includes code prototype that will be reorganized into # other modules -from . import * +from .defs import * -class StackFrame: +class StackFrame(object): def __init__(self, row, caller, count): self.row = row self.caller = caller @@ -12,7 +12,7 @@ def __init__(self, row, caller, count): self.returns = {} # repeat row -> number of rpts left # self.returns[row] = 0 -class Player: +class Player(object): class Flag: ROMAN = bit(0) @@ -139,7 +139,10 @@ def init(self): def refresh_devices(self): # determine output device support and load external programs - from . import support + # try: + import support + # except: + # import textbeat.support as support for dev in self.devices: if not support.supports(dev): print('device not supported by system: ' + dev) @@ -155,6 +158,8 @@ def set_rack(self, plugins): self.rack = 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) @@ -162,7 +167,13 @@ def add_flags(self, f): assert f > 0 else: for e in f: - self.add_flags(e) + 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): @@ -252,9 +263,11 @@ def run(self): # 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)) + cline = 'txbt> ('+str(int(self.tempo))+'bpm x'+str(int(self.grid))+' '+\ note_name(self.transpose + self.tracks[0].transpose) + ' ' +\ - orr(self.tracks[0].scale,self.scale).mode_name(orr(self.tracks[0].mode,self.mode,-1))+\ + ('diatonic' if modename=='ionian' else modename) + \ ')> ' # if bufline.endswith('.txbt'): # play file? @@ -334,7 +347,7 @@ def run(self): if tok[0]==' ': tok = tok[1:] var = tok[0].upper() - if var in 'TGXNPSRCKOFDR': # global vars % + if var in 'TGXNPSRCKFDR': # global vars % cmd = tok.split(' ')[0] op = cmd[1] try: @@ -371,7 +384,7 @@ def run(self): 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=='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 @@ -382,7 +395,7 @@ def run(self): if var=='K': self.transpose -= note_offset(val) print(note_offset(val)) - elif var=='O': self.octave -= int(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 @@ -400,7 +413,7 @@ def run(self): elif var=='D': self.devices = val.split(',') self.refresh_devices() - elif var=='O': self.octave = int(val) + # elif var=='O': self.octave = int(val) elif var=='N': self.grid=float(val)/4.0 #! elif var=='T': vals = val.split('x') @@ -428,8 +441,8 @@ def run(self): 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=='O': + # self.octave = int(val) elif var=='K': self.transpose = note_offset(val) # self.octave += -1*sgn(self.transpose)*(self.transpose//12) @@ -1232,7 +1245,7 @@ def run(self): cell = cell[count_seq(cell):] after = [] # after events - cl = len(cell) + clen = len(cell) # All tokens here must be listed in CCHAR ## + and - symbols are changed to mean minor and aug chords @@ -1243,7 +1256,7 @@ def run(self): # mn = n + base + (octave+shift) * 12 c = cell[0] c2 = None - if cl: + if clen: c2 = cell[:2] if c: c = c.lower() @@ -1301,7 +1314,7 @@ def run(self): shift = 1 ch.octave = octave # row_events += 1 - elif cl>1 and c=='~': # pitch wheel + elif clen>1 and c=='~': # pitch wheel cell = cell[1:] # sn = 1.0 if cell[0]=='/' or cell[0]=='\\': @@ -1365,15 +1378,15 @@ def run(self): # cell = cell[len(num):] # vel = int((float(num) / float('9'*len(num)))*127) # ch.cc(7,vel) - elif cell.startswith('Q'): # record sequence - cell = cell[1:] - r,ct = peel_uint(cell) - # ch.record(r) + elif cell.startswith('^^'): # record sequence + cell = cell[2:] + r,ct = peel_uint(cell,0) + ch.record(r) cell = cell[ct:] - elif cell.startswith('q'): # replay sequence + elif cell.startswith('^'): # replay sequence cell = cell[1:] - r,ct = peel_uint(cell) - # ch.replay(r) + r,ct = peel_uint(cell,0) + ch.replay(r) cell = cell[ct:] elif c2=='ch': # midi channel num,ct = peel_uint(cell[1:]) @@ -1387,8 +1400,7 @@ def run(self): # ch.soloed = True cell = cell[1:] elif c=='m': - ch.enabled = (c=='m') - ch.panic() + ch.enable(c=='m') cell = cell[1:] elif c=='c': # MIDI CC # get number @@ -1398,14 +1410,19 @@ def run(self): 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) elif c=='p': # program/patch change # bank select as other args? cell = cell[1:] - p,ct = peel_int(cell) - assert ct + p,ct = peel_uint(cell) + if not ct: + error('p command requires number') + cell = [] cell = cell[ct:] ch.patch(p) elif c2=='bs': # program/patch change @@ -1416,7 +1433,9 @@ def run(self): if cell and cell[0]==':': cell = cell[1:] num2,ct = peel_uint(cell) - assert ct + if not ct: + error(': params require number') + cell = [] cell = cell[ct:] b = num2 # second val -> lsb b |= num << 8 # first value -> msb @@ -1469,7 +1488,10 @@ def run(self): if ct: cell = cell[ct:] delay = -1*(c=='(')*float('0.'+num) if num else 0.5 - assert(delay > 0.0) # TOOD: impl early notes + if delay < 0.0: + error('delay >= 0') + cell = [] + continue elif c=='|': cell = [] elif c==':': diff --git a/presets/default.carxp b/textbeat/presets/default.carxp similarity index 100% rename from presets/default.carxp rename to textbeat/presets/default.carxp diff --git a/presets/example.carxp b/textbeat/presets/example.carxp similarity index 100% rename from presets/example.carxp rename to textbeat/presets/example.carxp diff --git a/presets/festivalrc b/textbeat/presets/festivalrc similarity index 100% rename from presets/festivalrc rename to textbeat/presets/festivalrc diff --git a/presets/test.xml b/textbeat/presets/test.xml similarity index 100% rename from presets/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 97% rename from src/schedule.py rename to textbeat/schedule.py index 1f43d44..399ae59 100644 --- a/src/schedule.py +++ b/textbeat/schedule.py @@ -1,12 +1,12 @@ -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 = [] diff --git a/src/support.py b/textbeat/support.py similarity index 72% rename from src/support.py rename to textbeat/support.py index 0ae0613..a38e15b 100644 --- a/src/support.py +++ b/textbeat/support.py @@ -1,14 +1,16 @@ -from . import * -from . import get_args +# TODO: eventually: scan and load plugins +from .defs import * from shutilwhich import which +import tempfile # from xml.dom import minidom ARGS = get_args() SUPPORT = set(['midi']) -SUPPORT_ALL = set(['carla','supercollider','csound','midi']) # gme,mpe -psonic = None +SUPPORT_ALL = set(['carla','supercollider','csound','midi', 'fluidsynth', 'sonicpi']) # gme,mpe +gen_inited = False if which('carla'): SUPPORT.add('carla') - SUPPORT.add('rack') # auto generate + SUPPORT.add('gen') # auto generate + gen_inited = True if which('scsynth'): try: @@ -17,16 +19,32 @@ except: pass +try: + import psonic + SUPPORT.add('sonicpi') +except ImportError: + pass + +try: + if which('fluidsynth'): + SUPPORT.add('fluidsynth') +except ImportError: + pass + csound = None -if which('csound'): +# if which('csound'): +try: + import csnd6 SUPPORT.add('csound') +except ImportError: + pass def supports(dev): global SUPPORT return dev in SUPPORT csound_inited = False -def csound_init(rack=[]): +def csound_init(gen=[]): global csound_inited if not csound_inited: import subprocess @@ -36,31 +54,38 @@ def csound_init(rack=[]): carla_inited = False carla_proc = None -def carla_init(rack): +carla_proj = None +def carla_init(gen): global carla_proc + global carla_proj + global carla_inited if not carla_proc: import oscpy fn = ARGS['SONGNAME'] if not fn: fn = 'default' - if devs: + if gen: # generate proj file from devs # embedded file -> /tmp/proj - proj = fn.split('.')[0]+'.carxp' # TEMP: generate + # carla_proj = proj = fn.split('.')[0]+'.carxp' # TEMP: generate + temp_dir = tempfile.gettempdir() + temp_path = os.path.join(temp_dir, os.path.join(os.path.abspath(sys.argv[0]),'presets','default.carxp')) + shutil.copy2(path, temp_path) else: proj = fn.split('.')[0]+'.carxp' if os.path.exists(proj): carla_proc = subprocess.Popen(['carla', '--nogui', proj], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - elif not devs: + elif not gen: log('To load a Carla project headless, create a \'%s\' file.' % proj) + carla_inited = True -def rack_init(rack): - carla_init(rack) +def gen_init(gen): + carla_init(gen) support_init = { 'csound': csound_init, 'carla': carla_init, - 'rack': rack_init, + 'gen': gen_init, } def csound_send(s): @@ -69,7 +94,7 @@ def csound_send(s): # 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 = {} @@ -124,4 +149,11 @@ def support_stop(): if BGPROC: BGPIPE.send((BGCMD.QUIT,)) BGPROC.join() + if gen_inited and carla_proj: + try: + os.remove(carla_proj[1]) + except OSError: + pass + except FileNotFoundError: + pass diff --git a/src/theory.py b/textbeat/theory.py similarity index 99% rename from src/theory.py rename to textbeat/theory.py index aef591c..b896d13 100644 --- a/src/theory.py +++ b/textbeat/theory.py @@ -2,7 +2,7 @@ import os, sys from future.utils import iteritems from collections import OrderedDict -from . import get_defs +from .defs import get_defs from .parser import * FLATS=False diff --git a/src/track.py b/textbeat/track.py similarity index 98% rename from src/track.py rename to textbeat/track.py index ef4902f..0a50bd9 100644 --- a/src/track.py +++ b/textbeat/track.py @@ -1,11 +1,11 @@ -from . import * +from .defs import * -class Recording: +class Recording(object): def __init__(self, name, slot): self.name = slot self.content = [] -class Tuplet: +class Tuplet(object): def __init__(self): # self.tuplets = False self.note_spacing = 1.0 @@ -82,6 +82,7 @@ def reset(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 @@ -90,10 +91,13 @@ def reset(self): 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< Date: Thu, 23 Aug 2018 15:47:58 -0700 Subject: [PATCH 31/59] fixing windows errors --- textbeat/__main__.py | 10 +++++----- textbeat/defs.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/textbeat/__main__.py b/textbeat/__main__.py index 250bad2..6a324ab 100755 --- a/textbeat/__main__.py +++ b/textbeat/__main__.py @@ -58,11 +58,11 @@ def main(): # from .support import * # from .support import * - style = style_from_dict({ - Token: '#ff0066', - Token.Prompt: '#00aa00', - Token.Info: '#000088', - }) + # style = style_from_dict({ + # Token: '#ff0066', + # Token.Prompt: '#00aa00', + # Token.Info: '#000088', + # }) colorama.init(autoreset=True) # logging.basicConfig(filename=LOG_FN,level=logging.DEBUG) diff --git a/textbeat/defs.py b/textbeat/defs.py index 42e1696..f539621 100644 --- a/textbeat/defs.py +++ b/textbeat/defs.py @@ -16,10 +16,10 @@ 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 From 92e94038c6a110f0e6f5fa5fe302d093b13c5f39 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Thu, 23 Aug 2018 15:55:42 -0700 Subject: [PATCH 32/59] .cmd for windows users --- txbt.cmd | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 txbt.cmd 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 From e7afcd3a5dd58d6f947db4d8d6a6ac8c1337e8cb Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Fri, 19 Oct 2018 18:32:16 -0700 Subject: [PATCH 33/59] fixes: arp, script, started: test synth, tutorial --- .pylintrc | 2 +- README.md | 60 +++++++------ examples/drums1.txbt | 7 ++ setup.py | 0 textbeat/__main__.py | 52 ++++++----- textbeat/player.py | 47 +++++++--- textbeat/plugins/synth.py | 176 ++++++++++++++++++++++++++++++++++++++ textbeat/support.py | 106 +++++++++++++++-------- textbeat/track.py | 5 +- textbeat/tutorial.py | 12 +++ txbt | 2 +- 11 files changed, 371 insertions(+), 98 deletions(-) create mode 100644 examples/drums1.txbt mode change 100755 => 100644 setup.py create mode 100755 textbeat/plugins/synth.py create mode 100644 textbeat/tutorial.py 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/README.md b/README.md index ea2dfb0..852aae0 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ Textbeat is a new project, 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 @@ -46,6 +46,8 @@ Textbeat is a new project, but you can already do lots of cool things: 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. +I'm currently working on headless VST rack generation. + If you want to use VST instruments, you'll need to route the MIDI out to something that hosts them, like a DAW. 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. @@ -321,9 +323,9 @@ Unlike accents, volume changes persist. Interpolation is not yet impl -## Articulation +## Vibrato, Pitch, and Mod Wheel -The vibrato symbol is a tilda (~). +To add vibrato to a note, suffix it with a tilda (~). Vibrato uses the mod wheel right now, but will eventually use pitch wheel oscillation. @@ -332,7 +334,7 @@ In the future, articulation will be programmable, per-track or per-song. ## 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& @@ -347,7 +349,7 @@ 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. @@ -367,7 +369,7 @@ The dots are placeholders. . 1'' ``` -Columns can be detected in some cases, but you'll probably want to +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. @@ -386,7 +388,8 @@ For best view in an editor, it is recommended that you offset the first column b ## Patches Another useful global var is 'p', which sets midi patches by name or number -across the tracks. The midi names support partial case-insensitive matches. +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 @@ -396,17 +399,18 @@ For a full list of GM names, see [def/gm.yaml](https://github.com/flipcoder/text ## Tuplets -Very early support for this. See tuplet example. -The 't' command spreads a set of notes across a tuplet grid, -starting at the first occurence of t in that group. +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. + +Tuplets are marked by 'T' and have an optional value at the first occurence 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. +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 @@ -419,8 +423,14 @@ Consider the 2 tracks: 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. + +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. + +The spaces that occur after (and between) tuplet groupings should remain empty, +since they are spacers to make the expansion line up. ## Picking @@ -435,7 +445,7 @@ to make them line up in a default ratio of 3:4. # to set a relative key, this will go from a major scale to relative minor scale %k+6 -# you can also go down to relative minor below +# you can also go downwards %k-6 # scale names are supported, this changes the scale shape to dorian @@ -447,10 +457,10 @@ to make them line up in a default ratio of 3:4. ## Chords (Advanced) -Slash chords do not imply inversions, -but are for stacking across octaves. Additionally, note names alone do no imply chords. +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 use shift operator (maj> for first inversion)) +interpret this as a specific chord voicing. Inversions in textbeat uses shift operator (>) instead (maj> for maj first inversion)) ``` b7maj7#4/sus2/1 @@ -468,7 +478,7 @@ Check out the examples/ folder. Play them with textbeat from the command line: ``` -./textbeat.py examples/jazz +./txbt examples/jazz.txbt ``` # Advanced @@ -635,7 +645,7 @@ Example: 1~ is fine, but 1v is not. Use 1@v You only need one to combine: 1@v5e5 Note: Fractional values specified are formated 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/def/default.yaml). +CC mapping is customizable inside [def/cc.yaml](https://github.com/flipcoder/textbeat/blob/master/textbeat/def/default.yaml). ``` @@ -659,9 +669,9 @@ CC mapping is customizable inside [def/cc.yaml](https://github.com/flipcoder/tex A majority of the music index is contained in inside these files: -- Default: [def/default.yaml](https://github.com/flipcoder/textbeat/blob/master/def/default.yaml). -- Informal: [def/informal.yaml](https://github.com/flipcoder/textbeat/blob/master/def/informal.yaml). -- Experimental: [def/exp.yaml](https://github.com/flipcoder/textbeat/blob/master/def/exp.yaml). +- 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.). 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/setup.py b/setup.py old mode 100755 new mode 100644 diff --git a/textbeat/__main__.py b/textbeat/__main__.py index 6a324ab..c6aa222 100755 --- a/textbeat/__main__.py +++ b/textbeat/__main__.py @@ -4,18 +4,19 @@ Open-source under MIT License Examples: - textbeat.py shell - textbeat.py song.txbt play song + textbeat shell + textbeat song.txbt play song Usage: - textbeat.py [--dev= | --verbose | --midi= | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME] - textbeat.py [+RANGE] [--dev= | --midi= | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME] - textbeat.py -c [COMMANDS ...] - textbeat.py -l [LINE_CONTENT ...] + textbeat [--dev= | --midi= | --ring | --follow --stdin] [-aeftnpsrxhvL] [SONGNAME] + textbeat [+RANGE] [--dev= | --midi= | --ring | --follow | --stdin] [-aeftnpsrxhvL] [SONGNAME] + 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] @@ -25,9 +26,10 @@ -f --flags comma-separated global flags -c execute commands sequentially -l execute commands simultaenously + --stdin read from stdin instead of file -r --remote (STUB) remote/daemon mode, keep alive --ring don't mute midi on end - --loop loop song + -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 @@ -41,7 +43,7 @@ --lint (STUB) analyze file --follow (old) print newlines every line, no output --quiet no output - --input (STUB) midi input chord analyzer + -a --analyze (STUB) midi input chord analyzer """ from __future__ import absolute_import, unicode_literals, print_function, generators # try: @@ -51,7 +53,7 @@ def main(): # if __name__!='__main__': # sys.exit(0) - ARGS = docopt(__doc__) + ARGS = docopt(__doc__.replace('TEXTBEAT',os.path.basename(sys.argv[0]).lower())) set_args(ARGS) from . import support @@ -110,11 +112,13 @@ def main(): 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 + # elif arg == '--renderman': player.renderman = True if player.cmdmode=='l': player.buf = ' '.join(ARGS['LINE_CONTENT']).split(';') # ; @@ -123,8 +127,14 @@ def main(): else: # mode n # if len(sys.argv)>=2: # FN = sys.argv[-1] - if ARGS['SONGNAME']: - FN = ARGS['SONGNAME'] + FN = ARGS['SONGNAME'] + from_stdin = False + if FN=='-' or ARGS['--stdin']: + FN = 0 # TEMP: doesnt 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 @@ -161,7 +171,7 @@ def main(): player.cmdmode = '' player.shell = True - player.interactive = player.shell or player.remote + player.interactive = player.shell or player.remote or player.tutorial pygame.midi.init() if pygame.midi.get_count()==0: @@ -258,20 +268,20 @@ def main(): active = support.SUPPORT_ALL & support.SUPPORT inactive = support.SUPPORT_ALL - support.SUPPORT if active: - log(FG.GREEN + 'Active Modules: ' + FG.WHITE + ', '.join(active) + FG.WHITE) + log(FG.GREEN + 'Active Modules: ' + STYLE.RESET_ALL + ', '.join(active) + STYLE.RESET_ALL) if inactive: - log(FG.RED + 'Inactive Modules: ' + FG.WHITE + ', '.join(inactive)) + log(FG.RED + 'Inactive Modules: ' + STYLE.RESET_ALL + ', '.join(inactive)) if player.portname: - log(FG.GREEN + 'Device: ' + FG.WHITE + '%s' % (player.portname if player.portname else 'Unknown',)) - log(FG.RED + 'Other Devices: ' + FG.WHITE + '%s' % (', '.join(portnames))) + 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: '+ FG.WHITE +'%s' % GM[player.tracks[0].patch_num]) + log(FG.GREEN + 'GM Patch: '+ STYLE.RESET_ALL +'%s' % GM[player.tracks[0].patch_num]) - log('Use -h for command line options.') - log('Read the manual and look at examples. Have fun!') + # log('') + # log(FG.BLUE + 'New? Type help and press enter to start the tutorial.') log('') player.run() @@ -279,7 +289,7 @@ def main(): if player.midifile: player.midifile.save(midifn) -# TODO: turn all midi note off + # TODO: turn all midi note off i = 0 for ch in player.tracks: if not player.ring: diff --git a/textbeat/player.py b/textbeat/player.py index 948b741..26797f4 100644 --- a/textbeat/player.py +++ b/textbeat/player.py @@ -47,6 +47,7 @@ def __init__(self): 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 @@ -54,6 +55,7 @@ def __init__(self): self.markers = {} f = StackFrame(-1,-1,0) f.returns[''] = 0 + self.tutorial = None self.callstack = [f] self.separators = [] self.track_history = ['.'] * NUM_TRACKS @@ -64,6 +66,7 @@ def __init__(self): self.stoprow = -1 self.cmdmode = 'n' # n normal c command s sequence self.schedule = Schedule(self) + self.rack = [] self.tracks = [] self.shell = False self.remote = False @@ -140,15 +143,18 @@ def init(self): def refresh_devices(self): # determine output device support and load external programs # try: - import support + from .support import supports, support_init # except: # import textbeat.support as support for dev in self.devices: - if not support.supports(dev): - print('device not supported by system: ' + dev) + if not supports(dev): + if dev!='auto': + print('Device not supported by system: ' + dev) + else: + print('Loading instrument presets requires Carla.') assert False try: - support.support_init[dev](self.rack) + support_init[dev](self.rack) except KeyError: # no init needed, silent pass @@ -265,9 +271,16 @@ def run(self): # ')> ' modename = orr(self.tracks[0].scale,self.scale).mode_name(orr(self.tracks[0].mode,self.mode,-1)) - cline = 'txbt> ('+str(int(self.tempo))+'bpm x'+str(int(self.grid))+' '+\ - note_name(self.transpose + self.tracks[0].transpose) + ' ' +\ - ('diatonic' if modename=='ionian' else modename) + \ + 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? @@ -1186,6 +1199,8 @@ def run(self): # 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: @@ -1198,6 +1213,7 @@ def run(self): 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 @@ -1209,9 +1225,6 @@ def run(self): vel = ch.vel stop = False - sustain = ch.sustain - - delay = 0.0 showtext = [] arpnotes = False arpreverse = False @@ -1358,11 +1371,13 @@ def run(self): cell = cell[ct:] elif c2=='__': sustain = ch.sustain = True + ch.arp_sustain = True cell = cell[2:] elif c2=='_-': # deprecated sustain = False cell = cell[2:] elif c=='_': + ch.arp_sustain = True sustain = True cell = cell[1:] # elif c=='v': # volume - moved to CC @@ -1386,14 +1401,16 @@ def run(self): elif cell.startswith('^'): # replay sequence cell = cell[1:] r,ct = peel_uint(cell,0) + if self.showtext: + showtext.append('play sequence: ' + num) ch.replay(r) cell = cell[ct:] elif c2=='ch': # midi channel num,ct = peel_uint(cell[1:]) cell = cell[1+ct:] - ch.midi_channel(num) if self.showtext: - showtext.append('channel') + showtext.append('midi channel: ' + num) + ch.midi_channel(num) elif c=='s': # solo if used by itself (?) # scale if given args @@ -1570,6 +1587,7 @@ def run(self): if not notes: # & restarts arp (if no note) ch.arp_restart() + # ch.arp_sustain = sustain else: arpnotes = True arppattern = [] @@ -1677,7 +1695,7 @@ def run(self): # pass # schedule=True - if notes and not tuplets: + if notes and not tuplets and not sustain: ch.release_all() for ev in events: @@ -1719,7 +1737,8 @@ def run(self): # if not schedule or (i==0 and strum>=EPSILON and delay 0.5 else f + 0.25 + SQUARE = lambda f: 1.0 if f < 0.5 else -1.0 + SAW = lambda f: math.fmod(f + 0.5, 1.0) + NOISE = lambda f: random.random() + + FRAMES = 512 + RATE = 44100 + CHANNELS = 1 + WIDTH = 2 + class Oscillator: + def __init__(self, synth, dest=True): + self.reset() + self.synth = synth + if dest: + self.dest = Synth.Oscillator(synth, False) + def reset(self): + self.amp = 0.0 + self.midinote = -1 + self.pitch = 0.0 + self.phase = 0.0 + self.vib_phase = 0.0 + self.mix = 1.0 + self.vib = 0.0 + self.func = Synth.SINE + self.t = 0 + self.next = False + self.dest = None + self.sign = 0 + self.ch = 0 + self.end = False + self.rate_crush = 1 + # self.bufs = None + # self.fx = None + # self.enabled = False + def note(self, n=0, v=1.0, **kwargs): + self.dest.func = kwargs.get('func', Synth.SINE) + self.dest.midinote = n + self.dest.pitch = Synth.midi_to_pitch(n) + self.dest.amp = v + self.next = True + print('note', n, v, self.dest.pitch) + # def on(self): + # self.note(0, 1.0) + def generate(self): + pass + def off(self): + self.dest.amp = 0.0 + self.dest.midinote = -1 + self.next = True + def swap(self): + nx = self.dest + self.reset() + nx.dest = self + return nx + def sample(self, n, block_recur=False): + osc = self + v = osc.amp * osc.func(osc.phase) * osc.mix + sgn = np.sign(v) + if self.next and (sgn==0 or (self.sign!=0 and sgn!=self.sign)): + if block_recur: + assert False + return osc, 0.0 + osc = osc.swap() + osc.sign = sgn + vib = math.sin(self.vib_phase) + self.vib_phase += self.vib / Synth.RATE + osc.phase += osc.rate_crush * osc.pitch / Synth.RATE + return osc.sample(True) + osc.sign = sgn + if not block_recur: + vib = math.sin(self.vib_phase) + self.vib_phase += self.vib / Synth.RATE + osc.phase += osc.rate_crush * (osc.pitch+vib) / Synth.RATE + return osc, v + def done(self): + return (self.next and self.dest.midinote==-1) or (not self.next and self.midinote==-1) + def __init__(self): + self.audio = pyaudio.PyAudio() + self.midinotes = [None] * 127 + self.crush = 256 + self.osc = 1 + self.osc_count = 0 + self.oscs = [Synth.Oscillator(self) for x in range(self.osc)] + self.buf = array('h', list(range(Synth.FRAMES))) + self.stream = self.audio.open( + format=self.audio.get_format_from_width(Synth.WIDTH), + channels=Synth.CHANNELS, + rate=Synth.RATE, + frames_per_buffer=Synth.FRAMES, + output=True, + stream_callback=self.callback() + ) + self.stream.start_stream() + @staticmethod + def midi_to_pitch(f): + return pow(2.0, (f - 69.0)/12.0) * 440.0 + def callback(self): + def internal_callback(in_data, frame_count, time_info, status): + for n in range(frame_count): + self.buf[n] = 0 + self.osc_count = 0 + for o in range(len(self.oscs)): + osc = self.oscs[o] + if osc.done(): + break + self.osc_count += 1 + vs = 0 + for n in range(frame_count // osc.rate_crush): + osc, smp = osc.sample(n) + self.oscs[o] = osc + v = self.fx(int(0x7fff * smp)) // self.crush * self.crush + for i in range(osc.rate_crush): + self.buf[n * osc.rate_crush + i] += v + return (bytes(self.buf), pyaudio.paContinue) + return internal_callback + # def run(self): + # self.stream.start_stream() + # while self.stream.is_active(): + # time.sleep(0.1) + def fx(self, v): + return v + def deinit(self): + for osc in self.oscs: + if osc.midinote >= 0: + osc.off() + while self.osc_count > 0: + time.sleep(0.1) + self.stream.stop_stream() + self.stream.close() + self.audio.terminate() + def note(self, n, v=1.0, **kwargs): + ch = kwargs.get('ch', 0) + func = kwargs.get('func', Synth.SINE) + o = 0 + for osc in self.oscs: + if osc.done(): + osc = self.oscs[o] + osc.note(n, v, func=func) + return o + o += 1 + osc = self.oscs[0] + osc.note(n, v, func=func) + return 0 + def off(self, **kwargs): + ch = kwargs.get('ch', None) + for osc in self.oscs: + if (ch==None) or ch == osc.ch: + if not osc.done(): + osc.off() + self.oscs.sort(key=lambda x: x.midinote==-1) + +if __name__=='__main__': + synth = Synth() + for i in range(3): + synth.note(60 + i * 2, func=Synth.SAW) + time.sleep(0.5) + synth.off() + time.sleep(0.5) + synth.deinit() + del synth + diff --git a/textbeat/support.py b/textbeat/support.py index a38e15b..256a0b0 100644 --- a/textbeat/support.py +++ b/textbeat/support.py @@ -1,29 +1,29 @@ # TODO: eventually: scan and load plugins from .defs import * from shutilwhich import which -import tempfile +import tempfile, shutil # from xml.dom import minidom ARGS = get_args() SUPPORT = set(['midi']) -SUPPORT_ALL = set(['carla','supercollider','csound','midi', 'fluidsynth', 'sonicpi']) # gme,mpe -gen_inited = False +SUPPORT_ALL = set(['carla','midi', 'fluidsynth']) # gme,mpe,sonicpi,supercollider,csound +auto_inited = False if which('carla'): SUPPORT.add('carla') - SUPPORT.add('gen') # auto generate - gen_inited = True + SUPPORT.add('auto') # auto generate + auto_inited = True -if which('scsynth'): - try: - import oscpy - SUPPORT.add('supercollider') - except: - pass +# if which('scsynth'): +# try: +# import oscpy +# SUPPORT.add('supercollider') +# except: +# pass -try: - import psonic - SUPPORT.add('sonicpi') -except ImportError: - pass +# try: +# import psonic +# SUPPORT.add('sonicpi') +# except ImportError: +# pass try: if which('fluidsynth'): @@ -54,38 +54,73 @@ def csound_init(gen=[]): carla_inited = False carla_proc = None -carla_proj = None +carla_temp_proj = None def carla_init(gen): global carla_proc - global carla_proj + global carla_temp_proj global carla_inited + global carla_temp + if not carla_proc: import oscpy fn = ARGS['SONGNAME'] if not fn: fn = 'default' if gen: - # generate proj file from devs - # embedded file -> /tmp/proj - # carla_proj = proj = fn.split('.')[0]+'.carxp' # TEMP: generate - temp_dir = tempfile.gettempdir() - temp_path = os.path.join(temp_dir, os.path.join(os.path.abspath(sys.argv[0]),'presets','default.carxp')) - shutil.copy2(path, temp_path) + carla_temp_proj = tempfile.mkstemp('.carxp',fn) + os.close(carla_temp_proj[0]) + carla_temp_proj = carla_temp_proj[1] + os.unlink(carla_temp_proj) + base_proj = os.path.join(os.path.join(os.path.dirname(os.path.abspath(__file__)),'presets','default.carxp')) + shutil.copy2(base_proj, carla_temp_proj) + + # add instruments to temp proj file + filebuf = '' + with open(carla_temp_proj,'r') as f: + filebuf = f.read() + instrumentxml = '' + i = 0 + for instrument in gen: + fnparts = instrument.split('.') + name = fnparts[0] + try: + ext = fnparts[1].upper() + except IndexError: + ext = 'LV2' + instrumentxml += '\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(carla_temp_proj,'w') as f: + f.write(filebuf) + + proj = carla_temp_proj else: proj = fn.split('.')[0]+'.carxp' if os.path.exists(proj): - carla_proc = subprocess.Popen(['carla', '--nogui', proj], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(proj) + carla_proc = subprocess.Popen(['carla',proj], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # '--nogui', elif not gen: log('To load a Carla project headless, create a \'%s\' file.' % proj) carla_inited = True -def gen_init(gen): +def auto_init(gen): carla_init(gen) support_init = { 'csound': csound_init, 'carla': carla_init, - 'gen': gen_init, + 'auto': auto_init, } def csound_send(s): @@ -142,6 +177,9 @@ def bgproc_run(con): # BGPROC.start() def support_stop(): + global carla_temp_proj + if carla_temp_proj: + os.unlink(carla_temp_proj) if csound and csound_proc: csound_proc.kill() if carla_proc: @@ -149,11 +187,11 @@ def support_stop(): if BGPROC: BGPIPE.send((BGCMD.QUIT,)) BGPROC.join() - if gen_inited and carla_proj: - try: - os.remove(carla_proj[1]) - except OSError: - pass - except FileNotFoundError: - pass + # if gen_inited and carla_proj: + # try: + # os.remove(carla_proj[1]) + # except OSError: + # pass + # except FileNotFoundError: + # pass diff --git a/textbeat/track.py b/textbeat/track.py index 0a50bd9..9adb267 100644 --- a/textbeat/track.py +++ b/textbeat/track.py @@ -286,7 +286,7 @@ def patch(self, p, stackidx=-1): assert False assert len(gmwords) > 0 if self.ctx.shell: - log(FG.GREEN + 'GM Patch: ' + FG.WHITE + gmwords[0]) + 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 @@ -330,7 +330,7 @@ def arp(self, notes, count=0, sustain=False, pattern=[], reverse=False): 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 = False + self.arp_sustain = sustain def arp_stop(self): self.arp_enabled = False self.release_all() @@ -364,6 +364,7 @@ def arp_next(self, stop_infinite=True): return bool(self.arp_note) 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 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/txbt b/txbt index 7e36d90..60887c8 100755 --- a/txbt +++ b/txbt @@ -1,2 +1,2 @@ #!/bin/bash -PYTHONPATH=`dirname $0` python -m textbeat "$*" +PYTHONPATH=`dirname $0` python -m textbeat $* From 797b00c5041d6206d803f3808502ee5819df32fb Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Fri, 21 Dec 2018 18:22:39 -0800 Subject: [PATCH 34/59] fixed arp octave not persisting --- textbeat/player.py | 4 ++-- textbeat/track.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/textbeat/player.py b/textbeat/player.py index 26797f4..903d3ce 100644 --- a/textbeat/player.py +++ b/textbeat/player.py @@ -1687,9 +1687,9 @@ def run(self): p = base if arpnotes: - ch.arp(notes, arpcount, sustain, arppattern, arpreverse) + ch.arp(notes, arpcount, sustain, arppattern, arpreverse, octave) arpnext = ch.arp_next(self.shell or self.cmdmode in 'lc') - notes = [ch.arp_note] + notes = [ch.arp_note - (octave*12)] # [HACK] first note already has frame octave offset above delay = ch.arp_delay # if not fcmp(delay): # pass diff --git a/textbeat/track.py b/textbeat/track.py index 9adb267..a09e3c2 100644 --- a/textbeat/track.py +++ b/textbeat/track.py @@ -318,11 +318,11 @@ def patch(self, p, stackidx=-1): def bank(self, b): self.ccs[0] = b self.cc(0,b) - def arp(self, notes, count=0, sustain=False, pattern=[], reverse=False): + 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 = notes + 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] @@ -361,7 +361,7 @@ def arp_next(self, stop_infinite=True): self.arp_idx = 0 # else: # self.arp_idx += 1 - return bool(self.arp_note) + return self.arp_note != None def arp_restart(self, count = None): self.arp_enabled = True # self.arp_sustain = False From 0f34afc9f72e04942a77c923f1bbfe5d5f7632a8 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Sat, 22 Dec 2018 16:09:28 -0800 Subject: [PATCH 35/59] moved broken temp synth plugin to another project --- textbeat/plugins/synth.py | 176 -------------------------------------- 1 file changed, 176 deletions(-) delete mode 100755 textbeat/plugins/synth.py diff --git a/textbeat/plugins/synth.py b/textbeat/plugins/synth.py deleted file mode 100755 index d856945..0000000 --- a/textbeat/plugins/synth.py +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env python -import sys, time, math, random -import pyaudio -import numpy as np -from array import array - -if __name__=='__main__': - class Device(object): - pass -else: - from .. import device - -class Synth(Device): - - SINE = lambda f: math.sin(math.tau * f) - TRIANGLE = lambda f: -f if math.fmod(f*2.0) > 0.5 else f + 0.25 - SQUARE = lambda f: 1.0 if f < 0.5 else -1.0 - SAW = lambda f: math.fmod(f + 0.5, 1.0) - NOISE = lambda f: random.random() - - FRAMES = 512 - RATE = 44100 - CHANNELS = 1 - WIDTH = 2 - class Oscillator: - def __init__(self, synth, dest=True): - self.reset() - self.synth = synth - if dest: - self.dest = Synth.Oscillator(synth, False) - def reset(self): - self.amp = 0.0 - self.midinote = -1 - self.pitch = 0.0 - self.phase = 0.0 - self.vib_phase = 0.0 - self.mix = 1.0 - self.vib = 0.0 - self.func = Synth.SINE - self.t = 0 - self.next = False - self.dest = None - self.sign = 0 - self.ch = 0 - self.end = False - self.rate_crush = 1 - # self.bufs = None - # self.fx = None - # self.enabled = False - def note(self, n=0, v=1.0, **kwargs): - self.dest.func = kwargs.get('func', Synth.SINE) - self.dest.midinote = n - self.dest.pitch = Synth.midi_to_pitch(n) - self.dest.amp = v - self.next = True - print('note', n, v, self.dest.pitch) - # def on(self): - # self.note(0, 1.0) - def generate(self): - pass - def off(self): - self.dest.amp = 0.0 - self.dest.midinote = -1 - self.next = True - def swap(self): - nx = self.dest - self.reset() - nx.dest = self - return nx - def sample(self, n, block_recur=False): - osc = self - v = osc.amp * osc.func(osc.phase) * osc.mix - sgn = np.sign(v) - if self.next and (sgn==0 or (self.sign!=0 and sgn!=self.sign)): - if block_recur: - assert False - return osc, 0.0 - osc = osc.swap() - osc.sign = sgn - vib = math.sin(self.vib_phase) - self.vib_phase += self.vib / Synth.RATE - osc.phase += osc.rate_crush * osc.pitch / Synth.RATE - return osc.sample(True) - osc.sign = sgn - if not block_recur: - vib = math.sin(self.vib_phase) - self.vib_phase += self.vib / Synth.RATE - osc.phase += osc.rate_crush * (osc.pitch+vib) / Synth.RATE - return osc, v - def done(self): - return (self.next and self.dest.midinote==-1) or (not self.next and self.midinote==-1) - def __init__(self): - self.audio = pyaudio.PyAudio() - self.midinotes = [None] * 127 - self.crush = 256 - self.osc = 1 - self.osc_count = 0 - self.oscs = [Synth.Oscillator(self) for x in range(self.osc)] - self.buf = array('h', list(range(Synth.FRAMES))) - self.stream = self.audio.open( - format=self.audio.get_format_from_width(Synth.WIDTH), - channels=Synth.CHANNELS, - rate=Synth.RATE, - frames_per_buffer=Synth.FRAMES, - output=True, - stream_callback=self.callback() - ) - self.stream.start_stream() - @staticmethod - def midi_to_pitch(f): - return pow(2.0, (f - 69.0)/12.0) * 440.0 - def callback(self): - def internal_callback(in_data, frame_count, time_info, status): - for n in range(frame_count): - self.buf[n] = 0 - self.osc_count = 0 - for o in range(len(self.oscs)): - osc = self.oscs[o] - if osc.done(): - break - self.osc_count += 1 - vs = 0 - for n in range(frame_count // osc.rate_crush): - osc, smp = osc.sample(n) - self.oscs[o] = osc - v = self.fx(int(0x7fff * smp)) // self.crush * self.crush - for i in range(osc.rate_crush): - self.buf[n * osc.rate_crush + i] += v - return (bytes(self.buf), pyaudio.paContinue) - return internal_callback - # def run(self): - # self.stream.start_stream() - # while self.stream.is_active(): - # time.sleep(0.1) - def fx(self, v): - return v - def deinit(self): - for osc in self.oscs: - if osc.midinote >= 0: - osc.off() - while self.osc_count > 0: - time.sleep(0.1) - self.stream.stop_stream() - self.stream.close() - self.audio.terminate() - def note(self, n, v=1.0, **kwargs): - ch = kwargs.get('ch', 0) - func = kwargs.get('func', Synth.SINE) - o = 0 - for osc in self.oscs: - if osc.done(): - osc = self.oscs[o] - osc.note(n, v, func=func) - return o - o += 1 - osc = self.oscs[0] - osc.note(n, v, func=func) - return 0 - def off(self, **kwargs): - ch = kwargs.get('ch', None) - for osc in self.oscs: - if (ch==None) or ch == osc.ch: - if not osc.done(): - osc.off() - self.oscs.sort(key=lambda x: x.midinote==-1) - -if __name__=='__main__': - synth = Synth() - for i in range(3): - synth.note(60 + i * 2, func=Synth.SAW) - time.sleep(0.5) - synth.off() - time.sleep(0.5) - synth.deinit() - del synth - From 45bd9e9c07b7d01037035be05d38080960f20ac4 Mon Sep 17 00:00:00 2001 From: David Briscoe Date: Wed, 20 Feb 2019 16:54:23 -0800 Subject: [PATCH 36/59] Add mido as dependency Fix error: Traceback (most recent call last): File "C:\apps\Python\Python37\lib\runpy.py", line 193, in _run_module_as_main "__main__", mod_spec) File "C:\apps\Python\Python37\lib\runpy.py", line 85, in _run_code exec(code, run_globals) File "E:\clones\textbeat\textbeat\__main__.py", line 50, in from .defs import * File "E:\clones\textbeat\textbeat\defs.py", line 10, in import mido ModuleNotFoundError: No module named 'mido' --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 49dd484..fe50968 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ packages=['textbeat','textbeat.def','textbeat.presets'], include_package_data=True, install_requires=[ - 'pygame','colorama','prompt_toolkit','appdirs','pyyaml','docopt','future','shutilwhich' + 'pygame','colorama','prompt_toolkit','appdirs','pyyaml','docopt','future','shutilwhich','mido' ], entry_points=''' [console_scripts] From 9ce2e549e15c75d73bd3ea24607a926af829efbb Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Tue, 19 Mar 2019 21:59:51 -0700 Subject: [PATCH 37/59] small progress on plugin system, print->out, -T --- textbeat/__main__.py | 29 ++++++++++++++-------------- textbeat/defs.py | 3 +++ textbeat/player.py | 45 +++++++++++++++++++++++++------------------- textbeat/support.py | 38 ++++++++++++++++++++++++++++++++++--- textbeat/track.py | 6 +++--- 5 files changed, 82 insertions(+), 39 deletions(-) diff --git a/textbeat/__main__.py b/textbeat/__main__.py index c6aa222..6470243 100755 --- a/textbeat/__main__.py +++ b/textbeat/__main__.py @@ -5,11 +5,13 @@ Examples: textbeat shell + textbeat -T start tutorial textbeat song.txbt play song Usage: - textbeat [--dev= | --midi= | --ring | --follow --stdin] [-aeftnpsrxhvL] [SONGNAME] - textbeat [+RANGE] [--dev= | --midi= | --ring | --follow | --stdin] [-aeftnpsrxhvL] [SONGNAME] + textbeat [--dev=] [--midi=] [--ring] [--follow] [--stdin] [-adeftnpsrxhvL] [INPUT] + textbeat [+RANGE] [--dev= | --midi= | --ring | --follow | --stdin] [-adeftnpsrxhvL] [INPUT] + textbeat [-rT] textbeat -c [COMMANDS ...] textbeat -l [LINE_CONTENT ...] @@ -26,8 +28,8 @@ -f --flags comma-separated global flags -c execute commands sequentially -l execute commands simultaenously - --stdin read from stdin instead of file - -r --remote (STUB) remote/daemon mode, keep alive + --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 @@ -112,8 +114,7 @@ def main(): 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 == '-T': player.tutorial = Tutorial(player) elif arg =='--flags': vals = val.split(',') player.add_flags(map(player.FLAGS.index, vals)) @@ -124,10 +125,10 @@ def main(): player.buf = ' '.join(ARGS['LINE_CONTENT']).split(';') # ; elif player.cmdmode=='c': player.buf = ' '.join(ARGS['COMMANDS']).split(' ') # spaces - else: # mode n + elif not player.tutorial: # mode n # if len(sys.argv)>=2: # FN = sys.argv[-1] - FN = ARGS['SONGNAME'] + FN = ARGS['INPUT'] from_stdin = False if FN=='-' or ARGS['--stdin']: FN = 0 # TEMP: doesnt work with py2 @@ -175,17 +176,17 @@ def main(): pygame.midi.init() if pygame.midi.get_count()==0: - print('No midi devices found.') + error('No midi devices found.') sys.exit(1) dev = -1 # if player.showtext: # for i in range(pygame.midi.get_count()): -# print(pygame.midi.get_device_info(i)) +# log(pygame.midi.get_device_info(i)) DEVS = get_defs()['dev'] if player.showtext: - print('MIDI Devices:') + log('MIDI Devices:') portnames = [] breakall = False firstpass = True @@ -196,7 +197,7 @@ def main(): if port[3]!=1: continue if player.showtext: - print(' '*4 + portname) + log(' '*4 + portname) if player.portname: if player.portname.lower() in portname.lower(): player.portname = portname @@ -224,7 +225,7 @@ def main(): # # continue # portname = port[1].decode('utf-8') # if player.showtext: -# print(' '*4 + portname) +# log(' '*4 + portname) # if player.portname: # if player.portname.lower() in portname.lower(): # player.portname = portname @@ -238,7 +239,7 @@ def main(): # break # portnames += [portname] if player.showtext: - print('') + log('') if dev == -1: dev = pygame.midi.get_default_output_id() diff --git a/textbeat/defs.py b/textbeat/defs.py index f539621..b6be773 100644 --- a/textbeat/defs.py +++ b/textbeat/defs.py @@ -132,6 +132,9 @@ def set_print(b): global PRINT PRINT = b +def out(msg): + if PRINT: + print(msg) def log(msg): if PRINT: print(msg) diff --git a/textbeat/player.py b/textbeat/player.py index 903d3ce..64861e2 100644 --- a/textbeat/player.py +++ b/textbeat/player.py @@ -1,5 +1,5 @@ -# TODO: This file includes code prototype that will be reorganized into -# other modules +# TODO: This player modulde includes parser and player prototype code +# that may eventually be reorganized into separate modules from .defs import * @@ -138,7 +138,7 @@ def init(self): embedded_fn = row[1:] embedded_file = [] - # print(self.embedded_files.keys()) + # out(self.embedded_files.keys()) def refresh_devices(self): # determine output device support and load external programs @@ -149,9 +149,9 @@ def refresh_devices(self): for dev in self.devices: if not supports(dev): if dev!='auto': - print('Device not supported by system: ' + dev) + out('Device not supported by system: ' + dev) else: - print('Loading instrument presets requires Carla.') + out('Loading instrument presets requires Carla.') assert False try: support_init[dev](self.rack) @@ -204,15 +204,15 @@ def follow(self): if self.startrow==-1 and self.canfollow: cursor = self.row + 1 if cursor != self.last_follow: - print(cursor) + out(cursor) self.last_cursor = cursor - # print(self.rowno[self.row]) + # out(self.rowno[self.row]) def pause(self): try: for ch in self.tracks[:self.tracks_active]: ch.release_all(True) - print('') + out('') input('PAUSED: Press ENTER to resume. Press Ctrl-C To quit.') except: return False @@ -351,7 +351,7 @@ def run(self): self.row += 1 continue - # TODO: global 'silent' commands (doesn't take time) + # 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(' '): @@ -407,7 +407,7 @@ def run(self): elif op=='-': if var=='K': self.transpose -= note_offset(val) - print(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)) @@ -505,7 +505,7 @@ def run(self): # self.transpose = 0 except NoSuchScale: - print(FG.RED + 'No such scale.') + out(FG.RED + 'No such scale.') pass else: assert False # no such var else: assert False # no such op @@ -677,6 +677,7 @@ def run(self): ch = self.tracks[cell_idx] fullcell = cell[:] ignore = False + skipcell = False # if self.instrument != ch.instrument: # self.player.set_instrument(ch.instrument) @@ -841,6 +842,9 @@ def run(self): 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 @@ -999,7 +1003,7 @@ def run(self): num,ct = peel_uint(tok[cut+1:]) if ct: - print(num) + out(num) cut += ct cut -= 2 # remove "no" chordname = chordname[:-2] # cut "no @@ -1033,9 +1037,9 @@ def run(self): # log(chordname) # don't include tuplet in chordname if 'add' in chordname: - # print(chordname) + # out(chordname) addtoks = chordname.split('add') - # print(addtoks) + # out(addtoks) chordname = addtoks[0] addnotes = addtoks[1:] @@ -1128,7 +1132,6 @@ def run(self): 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 @@ -1146,6 +1149,7 @@ def run(self): slashnotes[0].append(n + chord_root-1) + if expanded: if not chord_notes: # next chord @@ -1192,10 +1196,13 @@ def run(self): # 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 @@ -1595,7 +1602,7 @@ def run(self): if not (not arppattern and cell.startswith(':')) or\ (arppattern and cell.startswith('|')): break - print(cell[1:]) + out(cell[1:]) num,ct = peel_int(cell[1:],1) if not ct: break @@ -1623,8 +1630,8 @@ def run(self): denom = 1 << i if denom > num: break - # print('denom' + str(denom)) - # print('num ' + str(num)) + # out('denom' + str(denom)) + # out('num ' + str(num)) ch.note_spacing = denom/float(num) # ! ch.tuplet_count = int(num) ch.tuplet_offset = 0.0 diff --git a/textbeat/support.py b/textbeat/support.py index 256a0b0..f1760a4 100644 --- a/textbeat/support.py +++ b/textbeat/support.py @@ -1,15 +1,35 @@ -# TODO: eventually: scan and load plugins from .defs import * from shutilwhich import which import tempfile, shutil # from xml.dom import minidom ARGS = get_args() SUPPORT = set(['midi']) -SUPPORT_ALL = set(['carla','midi', 'fluidsynth']) # gme,mpe,sonicpi,supercollider,csound +SUPPORT_ALL = set(['carla', 'midi', 'fluidsynth', 'soundfonts']) # gme,mpe,sonicpi,supercollider,csound +MIDI = True +SOUNDFONTS = False # TODO: make this a SupportPlugin ref +AUTO = False auto_inited = False + +# TODO: eventually: scan and load plugins + +class PluginType: + NONE = 0 + AUTO = 1 + SOUNDFONTS = 2 + +class Plugin: + def __init__(self, name, typ): + self.name = name + self.type = typ + def register(self): + pass + +SUPPORT_PLUGINS = {} + if which('carla'): SUPPORT.add('carla') SUPPORT.add('auto') # auto generate + AUTO = True auto_inited = True # if which('scsynth'): @@ -27,7 +47,12 @@ try: if which('fluidsynth'): + import fluidsynth # https://github.com/flipcoder/pyfluidsynth SUPPORT.add('fluidsynth') + SUPPORT.add('soundfonts') + SOUNDFONTS = True +except AttributeError: + error("pyFluidSynth AttributeError detected. Use this pyFluidSynth version: https://github.com/flipcoder/pyfluidsynth") except ImportError: pass @@ -108,7 +133,7 @@ def carla_init(gen): else: proj = fn.split('.')[0]+'.carxp' if os.path.exists(proj): - print(proj) + log(proj) carla_proc = subprocess.Popen(['carla',proj], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # '--nogui', elif not gen: log('To load a Carla project headless, create a \'%s\' file.' % proj) @@ -176,6 +201,13 @@ def bgproc_run(con): # BGPROC = Process(target=bgproc_run, args=(child,)) # BGPROC.start() +def supports_soundfonts(): + return SOUNDFONTS +def supports_auto(): + return AUTO +def supports(tech): + return tech in SUPPORT + def support_stop(): global carla_temp_proj if carla_temp_proj: diff --git a/textbeat/track.py b/textbeat/track.py index a09e3c2..df46c2b 100644 --- a/textbeat/track.py +++ b/textbeat/track.py @@ -340,7 +340,7 @@ def arp_next(self, stop_infinite=True): # if not self.arp_enabled: # self.arp_note = None # return False - # print(self.arp_idx + 1) + # 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) @@ -374,7 +374,7 @@ def tuplet_next(self): delay = self.tuplet_offset self.tuplet_offset += self.note_spacing - 1.0 # if self.tuplet_offset >= 1.0 - EPSILON: - # print('!!!') + # out('!!!') # self.tuplet_offset = 0.0 self.tuplet_count -= 1 if not self.tuplet_count: @@ -383,7 +383,7 @@ def tuplet_next(self): # self.tuplet_stop() # if feq(delay,1.0): # return 0.0 - # print(delay) + # out(delay) return delay def tuplet_stop(self): self.tuplets = False From fe5c8abd27ffebd43d6ae1ab1e42c1f73dd294a5 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Fri, 17 May 2019 22:27:40 -0700 Subject: [PATCH 38/59] starting new plugin system + stubs --- textbeat/instrument.py | 24 +++++++++++++-- textbeat/plugins/__init__.py | 5 ++++ textbeat/plugins/sonicpi.py | 28 +++++++++++++++++ textbeat/plugins/supercollider.py | 32 ++++++++++++++++++++ textbeat/support.py | 50 +++++++++++++++++-------------- 5 files changed, 113 insertions(+), 26 deletions(-) create mode 100644 textbeat/plugins/__init__.py create mode 100755 textbeat/plugins/sonicpi.py create mode 100755 textbeat/plugins/supercollider.py diff --git a/textbeat/instrument.py b/textbeat/instrument.py index d16ac16..844c07b 100644 --- a/textbeat/instrument.py +++ b/textbeat/instrument.py @@ -1,5 +1,23 @@ - +#!/usr/bin/env python class Instrument(object): - - pass + def __init__(self,name): + self.name = name + def inited(self): + return False + def supported(self): + return False + def support(self): + return [] + def stop(self): + pass + +PLUGINS = [] +# plugins call this method +def export(s): + global PLUGINS + if s not in PLUGINS: + PLUGINS.append(s()) +def plugins(): + return PLUGINS + diff --git a/textbeat/plugins/__init__.py b/textbeat/plugins/__init__.py new file mode 100644 index 0000000..1fb4eb9 --- /dev/null +++ b/textbeat/plugins/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/python +import os +plugins = os.listdir(os.path.dirname(__file__)) +plugins = list(filter(lambda x: x not in ['__pycache__','__init__'], map(lambda x: x.split('.')[0], plugins))) +__all__ = plugins diff --git a/textbeat/plugins/sonicpi.py b/textbeat/plugins/sonicpi.py new file mode 100755 index 0000000..40f371d --- /dev/null +++ b/textbeat/plugins/sonicpi.py @@ -0,0 +1,28 @@ +#!/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): + Instrument.__init__(self, SonicPi.NAME) + self.enabled = False + def init(self): + self.enabled = True + def inited(self): + return self.enabled + def supported(self): + return not ERROR + def support(self): + return ['sonicpi'] + def stop(self): + pass + +instrument.export(SonicPi) + diff --git a/textbeat/plugins/supercollider.py b/textbeat/plugins/supercollider.py new file mode 100755 index 0000000..505df58 --- /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): + Instrument.__init__(self, SuperCollider.NAME) + self.enabled = False + def init(self): + self.enabled = True + def inited(self): + return self.enabled + def supported(self): + return not ERROR + def support(self): + return ['supercollider'] + def stop(self): + pass + +instrument.export(SuperCollider) + diff --git a/textbeat/support.py b/textbeat/support.py index f1760a4..9ad609f 100644 --- a/textbeat/support.py +++ b/textbeat/support.py @@ -1,6 +1,7 @@ from .defs import * from shutilwhich import which import tempfile, shutil +from . import instrument # from xml.dom import minidom ARGS = get_args() SUPPORT = set(['midi']) @@ -10,34 +11,31 @@ AUTO = False auto_inited = False -# TODO: eventually: scan and load plugins - -class PluginType: - NONE = 0 - AUTO = 1 - SOUNDFONTS = 2 - -class Plugin: - def __init__(self, name, typ): - self.name = name - self.type = typ - def register(self): - pass - SUPPORT_PLUGINS = {} +# load new-style plugins from plugins dir +from textbeat.plugins import * +# get plugins from instrument modules's export list +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_inited = True + +SUPPORT_ALL.add('carla') if which('carla'): SUPPORT.add('carla') SUPPORT.add('auto') # auto generate AUTO = True auto_inited = True - -# if which('scsynth'): -# try: -# import oscpy -# SUPPORT.add('supercollider') -# except: -# pass # try: # import psonic @@ -46,18 +44,20 @@ def register(self): # pass try: + SUPPORT_ALL.add('fluidsynth') if which('fluidsynth'): import fluidsynth # https://github.com/flipcoder/pyfluidsynth SUPPORT.add('fluidsynth') SUPPORT.add('soundfonts') SOUNDFONTS = True -except AttributeError: - error("pyFluidSynth AttributeError detected. Use this pyFluidSynth version: https://github.com/flipcoder/pyfluidsynth") +# except AttributeError: +# error("pyFluidSynth AttributeError detected. Use this pyFluidSynth version: https://github.com/flipcoder/pyfluidsynth") except ImportError: pass csound = None # if which('csound'): +SUPPORT_ALL.add('csound') try: import csnd6 SUPPORT.add('csound') @@ -219,6 +219,10 @@ def support_stop(): if BGPROC: BGPIPE.send((BGCMD.QUIT,)) BGPROC.join() + global plugs + for plug in plugs: + if plug.inited(): + plug.stop() # if gen_inited and carla_proj: # try: # os.remove(carla_proj[1]) From ce6f262bd1bde62fa394e963a9745f45c37c7dc2 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Sat, 18 May 2019 16:33:13 -0700 Subject: [PATCH 39/59] fluidsynth plugin stub, changed plugin exporting --- textbeat/instrument.py | 14 +++++----- textbeat/plugins/sonicpi.py | 9 +++--- textbeat/plugins/supercollider.py | 8 +++--- textbeat/support.py | 46 +++++++++++++++++++++---------- 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/textbeat/instrument.py b/textbeat/instrument.py index 844c07b..2bd73d5 100644 --- a/textbeat/instrument.py +++ b/textbeat/instrument.py @@ -13,11 +13,11 @@ def stop(self): pass PLUGINS = [] -# plugins call this method -def export(s): - global PLUGINS - if s not in PLUGINS: - PLUGINS.append(s()) -def plugins(): - return PLUGINS +# # plugins call this method +# def export(s): +# global PLUGINS +# if s not in PLUGINS: +# PLUGINS.append(s()) +# def plugins(): +# return PLUGINS diff --git a/textbeat/plugins/sonicpi.py b/textbeat/plugins/sonicpi.py index 40f371d..43ee587 100755 --- a/textbeat/plugins/sonicpi.py +++ b/textbeat/plugins/sonicpi.py @@ -12,11 +12,11 @@ class SonicPi(Instrument): NAME = 'sonicpi' def __init__(self): Instrument.__init__(self, SonicPi.NAME) - self.enabled = False + self.initalized = False def init(self): - self.enabled = True + self.initalized = True def inited(self): - return self.enabled + return self.initalized def supported(self): return not ERROR def support(self): @@ -24,5 +24,6 @@ def support(self): def stop(self): pass -instrument.export(SonicPi) +# instrument.export(SonicPi) +export = SonicPi diff --git a/textbeat/plugins/supercollider.py b/textbeat/plugins/supercollider.py index 505df58..390289a 100755 --- a/textbeat/plugins/supercollider.py +++ b/textbeat/plugins/supercollider.py @@ -16,11 +16,11 @@ class SuperCollider(Instrument): NAME = 'supercollider' def __init__(self): Instrument.__init__(self, SuperCollider.NAME) - self.enabled = False + self.initalized = False def init(self): - self.enabled = True + self.initalized = True def inited(self): - return self.enabled + return self.initalized def supported(self): return not ERROR def support(self): @@ -28,5 +28,5 @@ def support(self): def stop(self): pass -instrument.export(SuperCollider) +export = SuperCollider diff --git a/textbeat/support.py b/textbeat/support.py index 9ad609f..2c2cbf0 100644 --- a/textbeat/support.py +++ b/textbeat/support.py @@ -13,10 +13,19 @@ SUPPORT_PLUGINS = {} -# load new-style plugins from plugins dir +# load plugins from plugins dir + +import textbeat.plugins as tbp from textbeat.plugins import * -# get plugins from instrument modules's export list -plugs = instrument.plugins() +# search module exports for plugins +plugs = [] +for p in tbp.__dict__: + try: + pattr = getattr(tbp, p) + plugs += [pattr.export()] + except: + pass +# plugs = instrument.plugins() for plug in plugs: # plug.init() ps = plug.support() @@ -29,6 +38,11 @@ if 'auto' in s: AUTO = True auto_inited = True + if 'soundfonts' in s: + SOUNDFONTS = True + +# Note: the plugins below are old-style (contained within this file). New style +# plugins are in the plugins folder and are loaded above SUPPORT_ALL.add('carla') if which('carla'): @@ -43,17 +57,17 @@ # except ImportError: # pass -try: - SUPPORT_ALL.add('fluidsynth') - if which('fluidsynth'): - import fluidsynth # https://github.com/flipcoder/pyfluidsynth - SUPPORT.add('fluidsynth') - SUPPORT.add('soundfonts') - SOUNDFONTS = True -# except AttributeError: -# error("pyFluidSynth AttributeError detected. Use this pyFluidSynth version: https://github.com/flipcoder/pyfluidsynth") -except ImportError: - pass +# try: +# SUPPORT_ALL.add('fluidsynth') +# if which('fluidsynth'): +# import fluidsynth # https://github.com/flipcoder/pyfluidsynth +# SUPPORT.add('fluidsynth') +# SUPPORT.add('soundfonts') +# SOUNDFONTS = True +# # except AttributeError: +# # error("pyFluidSynth AttributeError detected. Use this pyFluidSynth version: https://github.com/flipcoder/pyfluidsynth") +# except ImportError: +# pass csound = None # if which('csound'): @@ -209,6 +223,7 @@ def supports(tech): return tech in SUPPORT def support_stop(): + # stop old-style plugins global carla_temp_proj if carla_temp_proj: os.unlink(carla_temp_proj) @@ -219,7 +234,8 @@ def support_stop(): if BGPROC: BGPIPE.send((BGCMD.QUIT,)) BGPROC.join() - global plugs + + # stop plugins from plugins folder for plug in plugs: if plug.inited(): plug.stop() From 9efac20f794659f53ba29584c05cee825885ddc3 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Sun, 19 May 2019 18:16:57 -0700 Subject: [PATCH 40/59] tutorial yaml and more plugin code migration and stubs --- textbeat/def/style.yaml | 1 + textbeat/player.py | 15 +-- textbeat/plugins/carla.py | 96 ++++++++++++++ textbeat/plugins/csound.py | 42 +++++++ textbeat/plugins/espeak.py | 90 ++++++++++++++ textbeat/plugins/fluidsynth.py | 49 ++++++++ textbeat/plugins/sonicpi.py | 8 +- textbeat/plugins/supercollider.py | 8 +- textbeat/support.py | 200 ++---------------------------- textbeat/tutorial.yaml | 175 ++++++++++++++++++++++++++ 10 files changed, 476 insertions(+), 208 deletions(-) create mode 100644 textbeat/def/style.yaml create mode 100755 textbeat/plugins/carla.py create mode 100755 textbeat/plugins/csound.py create mode 100755 textbeat/plugins/espeak.py create mode 100755 textbeat/plugins/fluidsynth.py create mode 100644 textbeat/tutorial.yaml diff --git a/textbeat/def/style.yaml b/textbeat/def/style.yaml new file mode 100644 index 0000000..9810217 --- /dev/null +++ b/textbeat/def/style.yaml @@ -0,0 +1 @@ +# playstyle variables like vibrato speed and depth diff --git a/textbeat/player.py b/textbeat/player.py index 64861e2..d16b819 100644 --- a/textbeat/player.py +++ b/textbeat/player.py @@ -66,7 +66,7 @@ def __init__(self): self.stoprow = -1 self.cmdmode = 'n' # n normal c command s sequence self.schedule = Schedule(self) - self.rack = [] + self.host = [] self.tracks = [] self.shell = False self.remote = False @@ -143,7 +143,7 @@ def init(self): def refresh_devices(self): # determine output device support and load external programs # try: - from .support import supports, support_init + from .support import supports, SUPPORT_PLUGINS # except: # import textbeat.support as support for dev in self.devices: @@ -151,17 +151,18 @@ def refresh_devices(self): if dev!='auto': out('Device not supported by system: ' + dev) else: - out('Loading instrument presets requires Carla.') + out('Loading instrument presets requires a compatible host module.') assert False try: - support_init[dev](self.rack) + # 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_rack(self, plugins): - self.rack = plugins + def set_host(self, plugins): + self.host = plugins self.refresh_devices() # def remove_flags(self, f): @@ -421,7 +422,7 @@ def run(self): elif var=='R': if not 'auto' in self.devices: self.devices = ['auto'] + self.devices - self.set_rack(val.split(',')) + self.set_host(val.split(',')) elif var=='V': self.version = val elif var=='D': self.devices = val.split(',') diff --git a/textbeat/plugins/carla.py b/textbeat/plugins/carla.py new file mode 100755 index 0000000..a22a8eb --- /dev/null +++ b/textbeat/plugins/carla.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +import textbeat.instrument as instrument +from textbeat.instrument import Instrument +from shutilwhich import which + +ERROR = False +if which('carla'): + ERROR = True + +class Carla(Instrument): + NAME = 'carla' + def __init__(self, args): + Instrument.__init__(self, Carla.NAME) + self.initialized = False + self.enabled = False + self.soundfonts = [] + self.proc = None + self.args = args + self.gen_inited = False + def enabled(self): + return self.initialized + def enable(self, rack): + if not self.proc: + fn = self.args['SONGNAME'] + if not fn: + fn = 'default' + if rack: + self.self.temp_proj = tempfile.mkstemp('.carxp',fn) + os.close(self.temp_proj[0]) + self.temp_proj = self.temp_proj[1] + os.unlink(self.temp_proj) + base_proj = os.path.join(os.path.join(os.path.dirname(os.path.abspath(__file__)),'presets','default.carxp')) + shutil.copy2(base_proj, self.temp_proj) + + # add instruments to temp proj file + filebuf = '' + with open(self.temp_proj,'r') as f: + filebuf = f.read() + instrumentxml = '' + i = 0 + for instrument in rack: + fnparts = instrument.split('.') + name = fnparts[0] + try: + ext = fnparts[1].upper() + except IndexError: + ext = 'LV2' + instrumentxml += '\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/textbeat/plugins/espeak.py b/textbeat/plugins/espeak.py new file mode 100755 index 0000000..534e4e2 --- /dev/null +++ b/textbeat/plugins/espeak.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +from textbeat.defs import * +import textbeat.instrument as instrument +from textbeat.instrument import Instrument +from shutilwhich import which +import subprocess + +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(object): + def __init__(self, con): + self.con = con + self.words = {} + self.processes = [] + def cache(self,word): + try: + tmp = self.words[word] + except: + tmp = tempfile.NamedTemporaryFile() + p = subprocess.Popen(['espeak', '\"'+pipes.quote(word)+'\"','--stdout'], stdout=tmp) + p.wait() + self.words[tmp.name] = tmp + return tmp + def run(self): + devnull = open(os.devnull, 'w') + while True: + msg = self.con.recv() + # log(msg) + if msg[0]==BGCMD.SAY: + tmp = self.cache(msg[1]) + # super slow, better option needed + self.processes.append(subprocess.Popen(['mpv','-ao','jack',tmp.name],stdout=devnull,stderr=devnull)) + elif msg[0]==BGCMD.CACHE: + self.cache(msg[1]) + elif msg[0]==BGCMD.QUIT: + break + elif msg[0]==BGCMD.CLEAR: + self.words.clear() + else: + log('BAD COMMAND: ' + msg[0]) + self.processses = list(filter(lambda p: p.poll()==None, self.processes)) + self.con.close() + for tmp in self.words: + tmp.close() + for proc in self.processes: + proc.wait() + +def bgproc_run(con): + 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() + + # self.proc.kill() + pass + +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 index 43ee587..950ff5c 100755 --- a/textbeat/plugins/sonicpi.py +++ b/textbeat/plugins/sonicpi.py @@ -10,13 +10,13 @@ class SonicPi(Instrument): NAME = 'sonicpi' - def __init__(self): + def __init__(self, args): Instrument.__init__(self, SonicPi.NAME) self.initalized = False - def init(self): + def enable(self): self.initalized = True - def inited(self): - return self.initalized + def enabled(self): + return self.initialized def supported(self): return not ERROR def support(self): diff --git a/textbeat/plugins/supercollider.py b/textbeat/plugins/supercollider.py index 390289a..261227a 100755 --- a/textbeat/plugins/supercollider.py +++ b/textbeat/plugins/supercollider.py @@ -14,13 +14,13 @@ class SuperCollider(Instrument): NAME = 'supercollider' - def __init__(self): + def __init__(self, args): Instrument.__init__(self, SuperCollider.NAME) self.initalized = False - def init(self): + def enable(self): self.initalized = True - def inited(self): - return self.initalized + def enabled(self): + return self.enabled def supported(self): return not ERROR def support(self): diff --git a/textbeat/support.py b/textbeat/support.py index 2c2cbf0..487eff1 100644 --- a/textbeat/support.py +++ b/textbeat/support.py @@ -5,10 +5,12 @@ # from xml.dom import minidom ARGS = get_args() SUPPORT = set(['midi']) -SUPPORT_ALL = set(['carla', 'midi', 'fluidsynth', 'soundfonts']) # gme,mpe,sonicpi,supercollider,csound +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 = {} @@ -22,7 +24,7 @@ for p in tbp.__dict__: try: pattr = getattr(tbp, p) - plugs += [pattr.export()] + plugs += [pattr.export(ARGS)] except: pass # plugs = instrument.plugins() @@ -37,184 +39,16 @@ SUPPORT_PLUGINS[s] = plug if 'auto' in s: AUTO = True + AUTO_MODULE = plug auto_inited = True if 'soundfonts' in s: SOUNDFONTS = True - -# Note: the plugins below are old-style (contained within this file). New style -# plugins are in the plugins folder and are loaded above - -SUPPORT_ALL.add('carla') -if which('carla'): - SUPPORT.add('carla') - SUPPORT.add('auto') # auto generate - AUTO = True - auto_inited = True - -# try: -# import psonic -# SUPPORT.add('sonicpi') -# except ImportError: -# pass - -# try: -# SUPPORT_ALL.add('fluidsynth') -# if which('fluidsynth'): -# import fluidsynth # https://github.com/flipcoder/pyfluidsynth -# SUPPORT.add('fluidsynth') -# SUPPORT.add('soundfonts') -# SOUNDFONTS = True -# # except AttributeError: -# # error("pyFluidSynth AttributeError detected. Use this pyFluidSynth version: https://github.com/flipcoder/pyfluidsynth") -# except ImportError: -# pass - -csound = None -# if which('csound'): -SUPPORT_ALL.add('csound') -try: - import csnd6 - SUPPORT.add('csound') -except ImportError: - pass + SOUNDFONT_MODULE = plug def supports(dev): global SUPPORT return dev in SUPPORT -csound_inited = False -def csound_init(gen=[]): - global csound_inited - if not csound_inited: - import subprocess - 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) - csound_inited = True - -carla_inited = False -carla_proc = None -carla_temp_proj = None -def carla_init(gen): - global carla_proc - global carla_temp_proj - global carla_inited - global carla_temp - - if not carla_proc: - import oscpy - fn = ARGS['SONGNAME'] - if not fn: - fn = 'default' - if gen: - carla_temp_proj = tempfile.mkstemp('.carxp',fn) - os.close(carla_temp_proj[0]) - carla_temp_proj = carla_temp_proj[1] - os.unlink(carla_temp_proj) - base_proj = os.path.join(os.path.join(os.path.dirname(os.path.abspath(__file__)),'presets','default.carxp')) - shutil.copy2(base_proj, carla_temp_proj) - - # add instruments to temp proj file - filebuf = '' - with open(carla_temp_proj,'r') as f: - filebuf = f.read() - instrumentxml = '' - i = 0 - for instrument in gen: - fnparts = instrument.split('.') - name = fnparts[0] - try: - ext = fnparts[1].upper() - except IndexError: - ext = 'LV2' - instrumentxml += '\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(carla_temp_proj,'w') as f: - f.write(filebuf) - - proj = carla_temp_proj - else: - proj = fn.split('.')[0]+'.carxp' - if os.path.exists(proj): - log(proj) - carla_proc = subprocess.Popen(['carla',proj], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # '--nogui', - elif not gen: - log('To load a Carla project headless, create a \'%s\' file.' % proj) - carla_inited = True - -def auto_init(gen): - carla_init(gen) - -support_init = { - 'csound': csound_init, - 'carla': carla_init, - 'auto': auto_init, -} - -def csound_send(s): - assert csound - return csound.sendto(s,('localhost',CSOUND_PORT)) - -# Currently not used, caches text to speech stuff in a way compatible with jack -# current super slow, need to write stabilizer first -class BackgroundProcess(object): - def __init__(self, con): - self.con = con - self.words = {} - self.processes = [] - def cache(self,word): - try: - tmp = self.words[word] - except: - tmp = tempfile.NamedTemporaryFile() - p = subprocess.Popen(['espeak', '\"'+pipes.quote(word)+'\"','--stdout'], stdout=tmp) - p.wait() - self.words[tmp.name] = tmp - return tmp - def run(self): - devnull = open(os.devnull, 'w') - while True: - msg = self.con.recv() - # log(msg) - if msg[0]==BGCMD.SAY: - tmp = self.cache(msg[1]) - # super slow, better option needed - self.processes.append(subprocess.Popen(['mpv','-ao','jack',tmp.name],stdout=devnull,stderr=devnull)) - elif msg[0]==BGCMD.CACHE: - self.cache(msg[1]) - elif msg[0]==BGCMD.QUIT: - break - elif msg[0]==BGCMD.CLEAR: - self.words.clear() - else: - log('BAD COMMAND: ' + msg[0]) - self.processses = list(filter(lambda p: p.poll()==None, self.processes)) - self.con.close() - for tmp in self.words: - tmp.close() - for proc in self.processes: - proc.wait() - -def bgproc_run(con): - proc = BackgroundProcess(con) - proc.run() - -BGPROC = None -# BGPIPE, child = Pipe() -# BGPROC = Process(target=bgproc_run, args=(child,)) -# BGPROC.start() - def supports_soundfonts(): return SOUNDFONTS def supports_auto(): @@ -223,27 +57,7 @@ def supports(tech): return tech in SUPPORT def support_stop(): - # stop old-style plugins - global carla_temp_proj - if carla_temp_proj: - os.unlink(carla_temp_proj) - if csound and csound_proc: - csound_proc.kill() - if carla_proc: - carla_proc.kill() - if BGPROC: - BGPIPE.send((BGCMD.QUIT,)) - BGPROC.join() - - # stop plugins from plugins folder for plug in plugs: if plug.inited(): plug.stop() - # if gen_inited and carla_proj: - # try: - # os.remove(carla_proj[1]) - # except OSError: - # pass - # except FileNotFoundError: - # pass - + 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. + From 07f311913975ce619339cb32088e69b03f270621 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Sun, 19 May 2019 18:37:24 -0700 Subject: [PATCH 41/59] style yaml, plugin readme message --- README.md | 6 ++++++ textbeat/def/style.yaml | 1 + 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index 852aae0..9817123 100644 --- a/README.md +++ b/README.md @@ -675,6 +675,12 @@ A majority of the music index is contained in inside these files: 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? I'm improving this faster than I'm documenting it. Because of that, not everything is explained. diff --git a/textbeat/def/style.yaml b/textbeat/def/style.yaml index 9810217..66ae429 100644 --- a/textbeat/def/style.yaml +++ b/textbeat/def/style.yaml @@ -1 +1,2 @@ # playstyle variables like vibrato speed and depth +style: From 7b27188041d0fe94dfe679af1b55cbc90732ba54 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Thu, 23 May 2019 15:48:55 -0700 Subject: [PATCH 42/59] -h fix, small changes for midifile support (not yet done) --- textbeat/__main__.py | 11 ++++++----- textbeat/player.py | 9 +++++++++ textbeat/track.py | 23 +++++++++++++++++++---- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/textbeat/__main__.py b/textbeat/__main__.py index 6470243..41dd509 100755 --- a/textbeat/__main__.py +++ b/textbeat/__main__.py @@ -9,9 +9,9 @@ textbeat song.txbt play song Usage: - textbeat [--dev=] [--midi=] [--ring] [--follow] [--stdin] [-adeftnpsrxhvL] [INPUT] - textbeat [+RANGE] [--dev= | --midi= | --ring | --follow | --stdin] [-adeftnpsrxhvL] [INPUT] - textbeat [-rT] + textbeat [--dev=] [--midi=] [--ring] [--follow] [--stdin] [-adeftnpsrxvL] [INPUT] + textbeat [+RANGE] [--dev=] [--midi=] [--ring] [--follow] [--stdin] [-adeftnpsrxL] [INPUT] + textbeat [-rhT] textbeat -c [COMMANDS ...] textbeat -l [LINE_CONTENT ...] @@ -36,7 +36,7 @@ + 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) + -H --transpose transpose (in half steps) --sustain start with sustain enabled --numbers use note numbers in output --notenames use note names in output @@ -55,7 +55,7 @@ def main(): # if __name__!='__main__': # sys.exit(0) - ARGS = docopt(__doc__.replace('TEXTBEAT',os.path.basename(sys.argv[0]).lower())) + ARGS = docopt(__doc__.replace('textbeat',os.path.basename(sys.argv[0]).lower())) set_args(ARGS) from . import support @@ -86,6 +86,7 @@ def main(): 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) diff --git a/textbeat/player.py b/textbeat/player.py index d16b819..17ca7da 100644 --- a/textbeat/player.py +++ b/textbeat/player.py @@ -511,6 +511,15 @@ def run(self): 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 diff --git a/textbeat/track.py b/textbeat/track.py index df46c2b..cc6c988 100644 --- a/textbeat/track.py +++ b/textbeat/track.py @@ -40,6 +40,9 @@ def __init__(self, ctx, idx, midich): self.initial_channel = midich self.non_drum_channel = midich self.reset() + def us(self): + # microseconds + return int(self.ctx.t)*1000000 def reset(self): Lane.reset(self) self.mode = 0 # 0 is NONE which inherits global mode @@ -179,13 +182,14 @@ def note_on(self, n, v=-1, sustain=False): 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: - self.midi[ch[0]].note_on(n,v,ch[1]) if self.ctx.midifile: while ch[0] >= len(self.ctx.midifile.tracks): self.ctx.midifile.tracks.append(mido.MidiTrack()) self.ctx.midifile.tracks[ch[0]].append(mido.Message( - 'note_on',velocity=v,time=int(self.ctx.t),channel=ch[1] + '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 @@ -195,10 +199,21 @@ def note_off(self, n, v=-1): # 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.midi[ch[0]].note_on(self.notes[n],0,ch[1]) - self.midi[ch[0]].note_off(self.notes[n],v,ch[1]) + 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: + while ch[0] >= len(self.ctx.midifile.tracks): + self.ctx.midifile.tracks.append(mido.MidiTrack()) + self.ctx.midifile.tracks[ch[0]].append(mido.Message( + 'note_on',note=n,velocity=0,time=self.us(),channel=ch[1] + )) + self.ctx.midifile.tracks[ch[0]].append(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: From c9d8f3dccf278981b3c06f705bfe5141a9a943d2 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Wed, 5 Jun 2019 13:15:48 -0700 Subject: [PATCH 43/59] more work on midifile support (still incomplete) --- textbeat/player.py | 10 +++++----- textbeat/track.py | 35 ++++++++++++++++++++++++----------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/textbeat/player.py b/textbeat/player.py index 17ca7da..73e9ecb 100644 --- a/textbeat/player.py +++ b/textbeat/player.py @@ -515,11 +515,11 @@ def run(self): 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.midifile.tracks[0].append(mido.MetaMessage( + 'set_tempo', tempo=mido.bpm2tempo(int( + val.split('x')[0] + )) + )) self.row += 1 continue diff --git a/textbeat/track.py b/textbeat/track.py index cc6c988..fc37903 100644 --- a/textbeat/track.py +++ b/textbeat/track.py @@ -140,6 +140,11 @@ def has_flags(self, f): # 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()) + self.ctx.midifile.tracks[ch].append(msg) def enable(self, v=True): was = v if not was and v: @@ -152,7 +157,10 @@ def stop(self): 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)) - self.midi[ch[0]].write_short(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 @@ -161,7 +169,10 @@ def panic(self): 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)) - self.midi[ch[0]].write_short(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 @@ -183,9 +194,7 @@ def note_on(self, n, v=-1, sustain=False): if (not self.ctx.muted or (self.ctx.muted and self.soloed))\ and self.enabled and self.ctx.startrow==-1: if self.ctx.midifile: - while ch[0] >= len(self.ctx.midifile.tracks): - self.ctx.midifile.tracks.append(mido.MidiTrack()) - self.ctx.midifile.tracks[ch[0]].append(mido.Message( + self.midifile_write(ch[0], mido.Message( 'note_on',note=n,velocity=v,time=self.us(),channel=ch[1] )) else: @@ -205,12 +214,10 @@ def note_off(self, n, v=-1): self.notes[n] = 0 self.sustain_notes[n] = 0 if self.ctx.midifile: - while ch[0] >= len(self.ctx.midifile.tracks): - self.ctx.midifile.tracks.append(mido.MidiTrack()) - self.ctx.midifile.tracks[ch[0]].append(mido.Message( + self.midifile_write(ch[0], mido.Message( 'note_on',note=n,velocity=0,time=self.us(),channel=ch[1] )) - self.ctx.midifile.tracks[ch[0]].append(mido.Message( + self.midifile_write(ch[0], mido.Message( 'note_off',note=n,velocity=v,time=self.us(),channel=ch[1] )) @@ -261,13 +268,19 @@ def pitch(self, val): # [-1.0,1.0] for ch in self.channels: status = (MIDI_PITCH<<4) + ch[1] if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: PITCH (%s, %s)' % (val,val2)) - self.midi[ch[0]].write_short(status,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)) - self.midi[ch[0]].write_short(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 From 2ef5556a350696fe00f52f50984118345e8098c2 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Mon, 8 Jul 2019 13:30:50 -0700 Subject: [PATCH 44/59] more wip on midi support, trivial fixes, comments --- textbeat/__main__.py | 5 +++-- textbeat/player.py | 17 ++++++++++++++--- textbeat/schedule.py | 2 +- textbeat/track.py | 5 ++++- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/textbeat/__main__.py b/textbeat/__main__.py index 41dd509..0afbdb7 100755 --- a/textbeat/__main__.py +++ b/textbeat/__main__.py @@ -43,7 +43,7 @@ --flats prefer flats in output (default) --sharps prefer sharps in output --lint (STUB) analyze file - --follow (old) print newlines every line, no output + --follow tracks file output for editors by printing newlines every line --quiet no output -a --analyze (STUB) midi input chord analyzer """ @@ -55,7 +55,8 @@ def main(): # if __name__!='__main__': # sys.exit(0) - ARGS = docopt(__doc__.replace('textbeat',os.path.basename(sys.argv[0]).lower())) + # ARGS = docopt(__doc__.replace('textbeat',os.path.basename(sys.argv[0]).lower())) + ARGS = docopt(__doc__) set_args(ARGS) from . import support diff --git a/textbeat/player.py b/textbeat/player.py index 73e9ecb..0a72344 100644 --- a/textbeat/player.py +++ b/textbeat/player.py @@ -201,6 +201,8 @@ def has_flags(self, f): return return self.flags & f + # for editor integration: "follows" the output of the file by printing + # newlines to stdout for every parsed line def follow(self): if self.startrow==-1 and self.canfollow: cursor = self.row + 1 @@ -226,6 +228,13 @@ def run(self): self.header = True embedded_file = False + # set initial midifile tempo + 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) + )) + while not self.quitflag: self.follow() @@ -695,7 +704,7 @@ def run(self): cell = cell.strip() if cell: - self.header = False + 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]) @@ -703,7 +712,7 @@ def run(self): self.track_history[cell_idx] = cell fullcell_sub = cell[:] - + # empty # if not cell: # cell_idx += 1 @@ -821,7 +830,9 @@ def run(self): # try to get roman numberal or number c,ct = peel_roman_s(tok) ambiguous = 0 - for amb in ('ion','dor','dim','dom','alt','dou','egy','aeo','dia','gui','bas','aug'): # TODO: make these auto + # 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) diff --git a/textbeat/schedule.py b/textbeat/schedule.py index 399ae59..bd289aa 100644 --- a/textbeat/schedule.py +++ b/textbeat/schedule.py @@ -61,8 +61,8 @@ def logic(self, t): slp = t*(1.0-self.passed) # remaining time if slp > 0.0: + self.ctx.t += self.ctx.speed*slp if self.ctx.cansleep and self.ctx.startrow == -1: - self.ctx.t += self.ctx.speed*slp time.sleep(self.ctx.speed*slp) self.passed = 0.0 diff --git a/textbeat/track.py b/textbeat/track.py index fc37903..6b19f2f 100644 --- a/textbeat/track.py +++ b/textbeat/track.py @@ -1,4 +1,5 @@ from .defs import * +import math class Recording(object): def __init__(self, name, slot): @@ -42,7 +43,8 @@ def __init__(self, ctx, idx, midich): self.reset() def us(self): # microseconds - return int(self.ctx.t)*1000000 + # 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 @@ -144,6 +146,7 @@ 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 From 8ae391fd4eb889093439b77dd2abc7152cdb1bd4 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Mon, 8 Jul 2019 13:54:11 -0700 Subject: [PATCH 45/59] fixing last commit that broke normal usage while testing midifile --- textbeat/player.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/textbeat/player.py b/textbeat/player.py index 0a72344..0944e9b 100644 --- a/textbeat/player.py +++ b/textbeat/player.py @@ -229,11 +229,12 @@ def run(self): embedded_file = False # set initial midifile tempo - 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) - )) + 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) + )) while not self.quitflag: self.follow() From affa155b3afec32450132b387549d7f21d97c1be Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Fri, 12 Jul 2019 13:34:59 -0700 Subject: [PATCH 46/59] more readme information, added some comments --- README.md | 42 +++++++++++++++++++--- textbeat/player.py | 88 +++++++++++++++++++++++++++++++++------------- 2 files changed, 101 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 9817123..23b5e98 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ but with syntax inspired by jazz/music theory. # Features -Textbeat 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 @@ -43,19 +43,51 @@ Textbeat is a new project, but you can already do lots of cool things: # Setup +## Linux + +``` +git clone https://github.com/flipcoder/textbeat +cd textbeat +sudo python setup.py install +textbeat +``` + +## Windows (Powershell) + +``` +git clone https://github.com/flipcoder/textbeat +cd textbeat +pip install -r requirements.txt +./txbt.cmd +``` + +## Test it out! + +Once you're in textbeat, try this: + +``` +maj& +``` + +If you don't hear 3 notes, you need to set up midi (this is the case with Linux). + +## How to set up midi + 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. -I'm currently working on headless VST rack generation. - 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.) 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. VSTs should work here as well. +If you're on Linux, you can use soundfonts through qsynth or use a software instrument like helm or dexed. I recommend qsynth. + +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 recording via a headless host. + +I'm currently looking into export options and recording via a headless host. # Tutorial diff --git a/textbeat/player.py b/textbeat/player.py index 0944e9b..a0f7bf4 100644 --- a/textbeat/player.py +++ b/textbeat/player.py @@ -221,13 +221,7 @@ def pause(self): return False return True - def run(self): - for ch in self.tracks: - ch.refresh() - - self.header = True - embedded_file = False - + def write_midi_tempo(self): # set initial midifile tempo if self.midifile: if not self.midifile.tracks: @@ -235,6 +229,15 @@ def run(self): 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() @@ -1315,11 +1318,12 @@ def run(self): # c = cell[1] # shift = int(c) if c.isdigit() else 0 # p = base + (octave+shift) * 12 - # INVERSION ct = 0 - if c2==';;': + # CELL COMMENTS + if c2==';;': # cell comment cell = [] break + #INVERSIONS elif c == '>' or c=='<': sign = (1 if c=='>' else -1) ct = count_seq(cell) @@ -1333,6 +1337,7 @@ def run(self): 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 @@ -1356,7 +1361,8 @@ def run(self): shift = 1 ch.octave = octave # row_events += 1 - elif clen>1 and c=='~': # pitch wheel + # PITCH WHEEL + elif clen>1 and c=='~': cell = cell[1:] # sn = 1.0 if cell[0]=='/' or cell[0]=='\\': @@ -1379,16 +1385,20 @@ def run(self): cell = cell[1:] vel = min(127,int(ch.vel + 0.5*(127.0-ch.vel))) ch.pitch(vel) - elif c == '~': # vibrato + # 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: @@ -1398,13 +1408,16 @@ def run(self): 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:] - elif c2=='_-': # deprecated + # [DEPRECATED] STOP SUSTAIN + elif c2=='_-': sustain = False cell = cell[2:] + # SUSTAIN elif c=='_': ch.arp_sustain = True sustain = True @@ -1422,32 +1435,38 @@ def run(self): # cell = cell[len(num):] # vel = int((float(num) / float('9'*len(num)))*127) # ch.cc(7,vel) - elif cell.startswith('^^'): # record sequence + # RECORD SEQ + elif cell.startswith('^^'): cell = cell[2:] r,ct = peel_uint(cell,0) ch.record(r) cell = cell[ct:] - elif cell.startswith('^'): # replay sequence + # 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:] - elif c2=='ch': # midi channel + # 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:] @@ -1462,7 +1481,8 @@ def run(self): cell = cell[ct:] ccval = int(num) ch.cc(cc,ccval) - elif c=='p': # program/patch change + # PROGRAM/PATCH CHANGE + elif c=='p': # bank select as other args? cell = cell[1:] p,ct = peel_uint(cell) @@ -1471,7 +1491,8 @@ def run(self): cell = [] cell = cell[ct:] ch.patch(p) - elif c2=='bs': # program/patch change + # BANK SELECT + elif c2=='bs': cell = cell[2:] num,ct = peel_uint(cell) cell = cell[ct:] @@ -1486,6 +1507,7 @@ def run(self): b = num2 # second val -> lsb b |= num << 8 # first value -> msb ch.bank(b) + # NOTE LENGTH elif c=='*': dots = count_seq(cell) if notes: @@ -1507,6 +1529,7 @@ def run(self): cell = cell[dots:] if self.showtext: showtext.append('duration(*)') + # PLACEHOLDER elif c=='.': dots = count_seq(cell) if len(c)>1 and notes: @@ -1527,6 +1550,7 @@ def run(self): cell = cell[dots:] if self.showtext: showtext.append('shorten(.)') + # NOTE TIME SHIFT elif c=='(' or c==')': # note shift (early/delay) num = '' cell = cell[1:] @@ -1538,11 +1562,14 @@ def run(self): error('delay >= 0') cell = [] continue + # MARKER (ignore -- already parsed) elif c=='|': cell = [] + # MARKER (ignore -- already parsed) elif c==':': cell = [] - elif c2=='!!': # full accent + # FULL ACCENT + elif c2=='!!': accent = '!!' cell = cell[2:] vel,ct = peel_uint_s(cell,127) @@ -1556,7 +1583,8 @@ def run(self): vel = 127 if self.showtext: showtext.append('accent(!!)') - elif c=='!': # accent + # ACCENT + elif c=='!': accent = '!' cell = cell[1:] # num,ct = peel_uint_s(cell) @@ -1570,7 +1598,8 @@ def run(self): vel = constrain(int(ch.vel + 0.5*(127.0-ch.vel)),127) if self.showtext: showtext.append('accent(!)') - elif c2=='??': # ghost + # GHOST NOTE + elif c2=='??': accent = '??' if ch.ghost_vel >= 0: vel = ch.ghost_vel @@ -1579,7 +1608,8 @@ def run(self): cell = cell[2:] if self.showtext: showtext.append('soften(??)') - elif c=='?': # soft + # SOFT NOTE + elif c=='?': accent = '?' if ch.soft_vel >= 0: vel = ch.soft_vel @@ -1589,7 +1619,8 @@ def run(self): if self.showtext: showtext.append('soften(?)') # elif cell.startswith('$$') or (c=='$' and lennotes==1): - elif c=='$': # strum/spread/tremolo + # STRUM/SPREAD/TREMOLO + elif c=='$': sq = count_seq(cell) cell = cell[sq:] num,ct = peel_uint_s(cell,'0') @@ -1605,6 +1636,7 @@ def run(self): ch.soft_vel = vel if self.showtext: showtext.append('strum($)') + # ARPEGGIO elif c=='&': count = count_seq(cell) arpcount,ct = peel_uint(cell[count:],0) @@ -1632,7 +1664,8 @@ def run(self): cell = cell[1+ct:] if self.showtext: showtext.append('arpeggio(&)') - elif c=='t': # tuplets + # TUPLETS + elif c=='t': tuplets = True tups = count_seq(cell,'t') Tups = count_seq(cell[tups:],'T') @@ -1661,10 +1694,12 @@ def run(self): # if not notes: # cell = [] # continue # ignore marker + # GLOABL 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) @@ -1676,6 +1711,7 @@ def run(self): 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) @@ -1691,6 +1727,7 @@ def run(self): else: ch.vel = num vel = num + # MIDI CC elif c in CC: cell = cell[1:] num,ct = peel_uint_s(cell) @@ -1700,6 +1737,7 @@ def run(self): 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) @@ -1737,7 +1775,7 @@ def run(self): strum -= strum if strum > EPSILON: ln = len(notes) - delta = (1.0/(ln*forr(duration,1.0))) #t between 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))) @@ -1766,6 +1804,7 @@ def run(self): # if not schedule or (i==0 and strum>=EPSILON and delay Date: Fri, 12 Jul 2019 13:58:29 -0700 Subject: [PATCH 47/59] fixed vim-textbeat not following lines --- textbeat/player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/textbeat/player.py b/textbeat/player.py index a0f7bf4..b52a2be 100644 --- a/textbeat/player.py +++ b/textbeat/player.py @@ -202,12 +202,12 @@ def has_flags(self, f): return self.flags & f # for editor integration: "follows" the output of the file by printing - # newlines to stdout for every parsed line + # 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: - out(cursor) + print(cursor) self.last_cursor = cursor # out(self.rowno[self.row]) From 9c8a95198b87946566cd720801b124e1259bc954 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Fri, 12 Jul 2019 16:57:34 -0700 Subject: [PATCH 48/59] windows instructions fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23b5e98..9efefa7 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ textbeat ``` git clone https://github.com/flipcoder/textbeat cd textbeat -pip install -r requirements.txt +pip3 install -r requirements.txt ./txbt.cmd ``` From eb21ba155930b52b8b7712f9bd8c91d64e8c2f3e Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Fri, 12 Jul 2019 16:58:20 -0700 Subject: [PATCH 49/59] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9efefa7..295690d 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ sudo python setup.py install textbeat ``` -## Windows (Powershell) +## Windows ``` git clone https://github.com/flipcoder/textbeat From e9f92b1edd2c448bb053d62f27d71ade565e954a Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Sat, 13 Jul 2019 21:51:49 -0700 Subject: [PATCH 50/59] updated gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 50bc2d0..200c438 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,9 @@ __pycache__/ build/ dist/ textbeat.egg-info/ +examples/*.mid +build.sh +clean.sh +*.old *.pyc +lint.txt From bf9fa50323b489113ace6e635e21283af86264d3 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Sun, 14 Jul 2019 18:55:07 -0700 Subject: [PATCH 51/59] fixed note omission in chord names --- textbeat/parser.py | 2 +- textbeat/player.py | 32 +++++++++++++++++++++++--------- textbeat/schedule.py | 15 +++++++++++---- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/textbeat/parser.py b/textbeat/parser.py index 05d680b..79d722f 100644 --- a/textbeat/parser.py +++ b/textbeat/parser.py @@ -93,7 +93,7 @@ def peel_any(s, match, d=''): ct += 1 else: break - return (ct,orr(r,d)) + return (orr(r,d),ct) def note_value(s): # turns dot note values (. and *) into frac if not s: diff --git a/textbeat/player.py b/textbeat/player.py index b52a2be..36d67f5 100644 --- a/textbeat/player.py +++ b/textbeat/player.py @@ -1017,28 +1017,42 @@ def run(self): try: # TODO: addchords - # TODO note removal (Maj7no5) + # TODO omit note (Maj7no5) if chordname[-2:]=='no': - numberpart = tok[cut+1:] + # print(tok) + # print(cut) + cut += 1 # The 'o' of no + numberpart = tok[cut:] + # print(numberpart) # second check will throws - if numberpart in '#b' or (int(numberpart) or True): + 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+1:]) + num,ct = peel_uint(tok[cut:]) if ct: - out(num) cut += ct - cut -= 2 # remove "no" - chordname = chordname[:-2] # cut "no - nonotes.append(str(prefix)+str(num)) # ex: b5 + # 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('chordname length ' + str(len(chordname))) + log('bad chordname index') pass # chordname length except ValueError: log('bad cast ' + char) diff --git a/textbeat/schedule.py b/textbeat/schedule.py index bd289aa..32de82d 100644 --- a/textbeat/schedule.py +++ b/textbeat/schedule.py @@ -14,6 +14,7 @@ def __init__(self, ctx): # 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): @@ -30,6 +31,12 @@ 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) @@ -50,8 +57,8 @@ def logic(self, t): # sleep until next event if ev.t >= 0.0: if self.ctx.cansleep and self.ctx.startrow == -1: - self.ctx.t += self.ctx.speed*t*(ev.t-self.passed) - time.sleep(self.ctx.speed*t*(ev.t-self.passed)) + 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: @@ -59,11 +66,11 @@ def logic(self, t): processed += 1 - slp = t*(1.0-self.passed) # remaining time + slp = t * (1.0 - self.passed) # remaining time if slp > 0.0: self.ctx.t += self.ctx.speed*slp if self.ctx.cansleep and self.ctx.startrow == -1: - time.sleep(self.ctx.speed*slp) + time.sleep(max(0,self.ctx.speed*slp)) self.passed = 0.0 self.events = self.events[processed:] From 921b51aa8a76865dd095bd8602c063d9a67e9b25 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Sun, 14 Jul 2019 19:42:30 -0700 Subject: [PATCH 52/59] fixed GM yaml link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 295690d..1ed5b51 100644 --- a/README.md +++ b/README.md @@ -427,7 +427,7 @@ matches of GM instruments. %t120 x2 p=piano,guitar,bass,drums c8,-2 ``` -For a full list of GM names, see [def/gm.yaml](https://github.com/flipcoder/textbeat/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). ## Tuplets From 974b567ce1c8ea9ce182a45bd746702deea147f6 Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Sat, 30 May 2020 14:09:27 -0700 Subject: [PATCH 53/59] setup file missing plugins folder --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fe50968..ef3cb34 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ author='Grady O\'Connell', author_email='flipcoder@gmail.com', license='MIT', - packages=['textbeat','textbeat.def','textbeat.presets'], + packages=['textbeat','textbeat.def','textbeat.presets','textbeat.plugins'], include_package_data=True, install_requires=[ 'pygame','colorama','prompt_toolkit','appdirs','pyyaml','docopt','future','shutilwhich','mido' From b64c12970f5ac87bbe3ff522d0f360497d7359ea Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Mon, 15 Jun 2020 01:35:50 -0700 Subject: [PATCH 54/59] --vi mode, added mido dep --- requirements.txt | 1 + textbeat/__main__.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index aa981a4..5f5594c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ pyyaml docopt future shutilwhich +mido diff --git a/textbeat/__main__.py b/textbeat/__main__.py index 0afbdb7..a568f0d 100755 --- a/textbeat/__main__.py +++ b/textbeat/__main__.py @@ -9,8 +9,8 @@ textbeat song.txbt play song Usage: - textbeat [--dev=] [--midi=] [--ring] [--follow] [--stdin] [-adeftnpsrxvL] [INPUT] - textbeat [+RANGE] [--dev=] [--midi=] [--ring] [--follow] [--stdin] [-adeftnpsrxL] [INPUT] + 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 ...] From aa3fba02c60c691cf6a868412488a51c83fee26e Mon Sep 17 00:00:00 2001 From: ccn Date: Wed, 21 Apr 2021 16:25:11 -0400 Subject: [PATCH 55/59] Update README.md Make it more explicit that a file is required and how to compile it for beginners. --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ed5b51..f0c53e9 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,9 @@ 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. @@ -128,6 +130,14 @@ The grid is the beat/quarter-note subdivision. Both Tempo and Grid can be decimal numbers as well. +You can listen to what you've made by running: + +``` +textbeat +``` + +Consult the output of `textbeat -h` for further information. + ## Note Numbers Both note numbers and letters are supported. From d10a1005a358350a7c5dd82af78bfed8be8ca3df Mon Sep 17 00:00:00 2001 From: ccn Date: Wed, 21 Apr 2021 16:45:09 -0400 Subject: [PATCH 56/59] Update README.md fix minor type on implemented --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0c53e9..d6d7baf 100644 --- a/README.md +++ b/README.md @@ -363,7 +363,7 @@ If you wish to control volume/gain directly, use @v Unlike accents, volume changes persist. -Interpolation is not yet impl +Interpolation is not yet implemented ## Vibrato, Pitch, and Mod Wheel From 52c29dcc29f6bf4d349b97dfad6e9af6304e4e3d Mon Sep 17 00:00:00 2001 From: ccn Date: Tue, 27 Apr 2021 16:22:38 -0400 Subject: [PATCH 57/59] Add use case for fractional time-steps I just had this use case right now - so thought I'd add it in. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index d6d7baf..8da3b74 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,9 @@ 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`. You can listen to what you've made by running: From 6e62245c7fba43544034ddde548aff19bf4e722e Mon Sep 17 00:00:00 2001 From: Grady O'Connell Date: Sun, 26 Dec 2021 13:32:19 -0800 Subject: [PATCH 58/59] Fixed collections.Mapping bug --- textbeat/defs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/textbeat/defs.py b/textbeat/defs.py index b6be773..4b2e934 100644 --- a/textbeat/defs.py +++ b/textbeat/defs.py @@ -160,7 +160,7 @@ class Diff: def merge(a, b, overwrite=False, skip=None, diff=None, pth=None): for k,v in iteritems(b): contains = k in a - if contains and isinstance(a[k], dict) and isinstance(b[k], collections.Mapping): + if contains and isinstance(a[k], dict) and isinstance(b[k], collections.abc.Mapping): loc = (pth+[k]) if pth else None if callable(skip): if not skip(loc,v): From 8f0d8d9c55278122407315bdccc25a35598f7d59 Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Wed, 15 Mar 2023 11:46:12 +0800 Subject: [PATCH 59/59] Fix typos Found via `codespell -L halfs,forr,clen` --- README.md | 10 +++++----- textbeat/__main__.py | 4 ++-- textbeat/parser.py | 2 +- textbeat/player.py | 12 ++++++------ textbeat/plugins/espeak.py | 2 +- textbeat/plugins/sonicpi.py | 4 ++-- textbeat/plugins/supercollider.py | 4 ++-- textbeat/theory.py | 2 +- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 8da3b74..6e0d2df 100644 --- a/README.md +++ b/README.md @@ -230,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. @@ -447,7 +447,7 @@ For a full list of GM names, see [def/gm.yaml](https://github.com/flipcoder/text 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. -Tuplets are marked by 'T' and have an optional value at the first occurence 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). @@ -641,7 +641,7 @@ To do relative values, drop the equals sign: - midi channels exceeding max value will be spanned across outputs - p: program assign - Set program to a given number - - Global var (%) p is usually prefered for string matching + - 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 @@ -687,7 +687,7 @@ 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 formated like numbers after a decimal point: +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). @@ -757,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 diff --git a/textbeat/__main__.py b/textbeat/__main__.py index a568f0d..a2f5525 100755 --- a/textbeat/__main__.py +++ b/textbeat/__main__.py @@ -27,7 +27,7 @@ -p --patch= (STUB) default midi patch, partial match -f --flags comma-separated global flags -c execute commands sequentially - -l execute commands simultaenously + -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 @@ -133,7 +133,7 @@ def main(): FN = ARGS['INPUT'] from_stdin = False if FN=='-' or ARGS['--stdin']: - FN = 0 # TEMP: doesnt work with py2 + FN = 0 # TEMP: doesn't work with py2 from_stdin = True else: from_stdin = False diff --git a/textbeat/parser.py b/textbeat/parser.py index 79d722f..2a48746 100644 --- a/textbeat/parser.py +++ b/textbeat/parser.py @@ -37,7 +37,7 @@ def peel_uint_s(s, d=None): def peel_roman_s(s, d=None): nums = 'ivx' r = '' - case = -1 # -1 unknown, 0 low, 1 uppper + case = -1 # -1 unknown, 0 low, 1 upper for ch in s: chl = ch.lower() chcase = (chl==ch) diff --git a/textbeat/player.py b/textbeat/player.py index 36d67f5..4ccb7b8 100644 --- a/textbeat/player.py +++ b/textbeat/player.py @@ -479,7 +479,7 @@ def run(self): try: if val: val = val.lower() - # ambigous alts + # ambiguous alts if val.isdigit(): modescale = (self.scale.name,int(val)) @@ -831,7 +831,7 @@ def run(self): tok = tok[1:] if not expanded: cell = cell[1:] - # try to get roman numberal or number + # try to get roman numeral or number c,ct = peel_roman_s(tok) ambiguous = 0 # Help parser ambiguities (TODO: make these automatic) @@ -985,7 +985,7 @@ def run(self): except ValueError: ignore = True else: - ignore = True # reenable if there's a chord listed + ignore = True # re-enable if there's a chord listed # CHORDS addnotes = [] @@ -1168,7 +1168,7 @@ def run(self): # chordnoteslist.append(chord_notes) # chordrootslist.append(chord_root) chord_root = n - ignore = False # reenable default root if chord was w/o note name + ignore = False # re-enable default root if chord was w/o note name continue else: pass @@ -1252,7 +1252,7 @@ def run(self): if ch.arp_enabled: if notes: # incoming notes? # log(notes) - # interupt arp + # interrupt arp ch.arp_stop() else: # continue arp @@ -1708,7 +1708,7 @@ def run(self): # if not notes: # cell = [] # continue # ignore marker - # GLOABL VARS (ignore -- already parsed) + # GLOBAL VARS (ignore -- already parsed) elif c=='%': # ctrl line cell = [] diff --git a/textbeat/plugins/espeak.py b/textbeat/plugins/espeak.py index 534e4e2..05cbd0a 100755 --- a/textbeat/plugins/espeak.py +++ b/textbeat/plugins/espeak.py @@ -42,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() diff --git a/textbeat/plugins/sonicpi.py b/textbeat/plugins/sonicpi.py index 950ff5c..f48a8e1 100755 --- a/textbeat/plugins/sonicpi.py +++ b/textbeat/plugins/sonicpi.py @@ -12,9 +12,9 @@ class SonicPi(Instrument): NAME = 'sonicpi' def __init__(self, args): Instrument.__init__(self, SonicPi.NAME) - self.initalized = False + self.initialized = False def enable(self): - self.initalized = True + self.initialized = True def enabled(self): return self.initialized def supported(self): diff --git a/textbeat/plugins/supercollider.py b/textbeat/plugins/supercollider.py index 261227a..4a07bfe 100755 --- a/textbeat/plugins/supercollider.py +++ b/textbeat/plugins/supercollider.py @@ -16,9 +16,9 @@ class SuperCollider(Instrument): NAME = 'supercollider' def __init__(self, args): Instrument.__init__(self, SuperCollider.NAME) - self.initalized = False + self.initialized = False def enable(self): - self.initalized = True + self.initialized = True def enabled(self): return self.enabled def supported(self): diff --git a/textbeat/theory.py b/textbeat/theory.py index b896d13..1ca4882 100644 --- a/textbeat/theory.py +++ b/textbeat/theory.py @@ -95,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']