From dced59a6820c442efa393bf3d0344e45f9201380 Mon Sep 17 00:00:00 2001
From: Grady O'Connell <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 @@
     +<range>              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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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<<x,x) for x in range(16)]), lambda x: x[1]))
@@ -59,7 +61,14 @@ def add_flags(self, f):
         if f != f & FLAGS:
             raise ParseError('invalid flags')
         self.flags |= f
-    def mute(self):
+    def enable(self, v=True):
+        was = v
+        if not was and v:
+            self.enabled = v
+            self.panic()
+    def disable(self, v=True):
+        self.enable(not v)
+    def stop(self):
         for ch in self.channels:
             status = (MIDI_CC<<4) + ch
             if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status,120,0))
@@ -90,7 +99,9 @@ def note_on(self, n, v=-1, sustain=False):
             self.sustain_notes[n] = sustain
             # log("on " + str(n))
             if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: NOTE ON (%s, %s, %s)' % (n,v,ch))
-            self.player.note_on(n,v,ch)
+            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)
     def note_off(self, n, v=-1):
         if v == -1:
             v = self.vel

From 3a27ebe0c2bfa92a85eb529c23306e814baa5123 Mon Sep 17 00:00:00 2001
From: Grady O'Connell <flipcoder@gmail.com>
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<<x,x) for x in range(16)]), lambda x: x[1]))
+    def volume(self,v=None):
+        if v==None:
+            return self.volval
+        self.volval = v
+        self.cc(7,int(v*127.0))
+    def refresh(self):
+        self.cc(1,0)
+        self.volume(self.volval)
     def add_flags(self, f):
         if f != f & FLAGS:
             raise ParseError('invalid flags')
@@ -74,7 +83,7 @@ def stop(self):
             if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status,120,0))
             self.player.write_short(status, 120, 0)
             if self.modval>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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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=<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 <flipcoder@gmail.com>
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=<fn> | --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=<fn>           generate midi file
     +<range>              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=<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<EPSILON):
-                    ch.note_on(p + n, vel, sustain)
-                else:
-                    f = lambda _,ch=ch,p=p,n=n,vel=vel,sustain=sustain: ch.note_on(p + n, vel, sustain)
-                    dc.schedule.add(Event(delay,f,ch))
-                delay += delta
-                i += 1
-            
-            cell_idx += 1
- 
-        while True:
-            try:
-                if not ctrl and not header:
-                    dc.schedule.logic(60.0 / dc.tempo / dc.grid)
-                    break
-                else:
-                    break
-            except KeyboardInterrupt:
-                # log(FG.RED + traceback.format_exc())
-                dc.quitflag = True
-                break
-            except:
-                log(FG.RED + traceback.format_exc())
-                if dc.shell:
-                    dc.quitflag = True
-                    break
-                if not dc.shell and not dc.pause():
-                    dc.quitflag = True
-                    break
-
-        if dc.quitflag:
-            break
-         
-    except KeyboardInterrupt:
-        dc.quitflag = True
-        break
-    except SignalError:
-        dc.quitflag = True
-        break
-    except:
-        log(FG.RED + traceback.format_exc())
-        if dc.shell:
-            dc.quitflag = True
-            break
-        if not dc.shell and not dc.pause():
-            break
+dc.run()
 
-    dc.row += 1
+if dc.midifile:
+    dc.save(midifn)
 
 # TODO: turn all midi note off
 i = 0
diff --git a/def/dc.yaml b/def/dc.yaml
index af75a4d..8f5f9e0 100644
--- a/def/dc.yaml
+++ b/def/dc.yaml
@@ -8,26 +8,28 @@ chords:
     wu7: '3 4 5 7'
     wu7b5: '2 3 b5 7'
     wu-: 'b3 4 5'
-    wu-7: 'b3 4 5 7'
-    wu-7b5: 'b3 4 b5 7'
+    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
-    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<EPSILON):
+                            ch.note_on(p + n, vel, sustain)
+                        else:
+                            f = lambda _,ch=ch,p=p,n=n,vel=vel,sustain=sustain: ch.note_on(p + n, vel, sustain)
+                            self.schedule.add(Event(delay,f,ch))
+                        delay += delta
+                        i += 1
+                    
+                    cell_idx += 1
+         
+                while True:
+                    try:
+                        if not ctrl and not self.header:
+                            self.schedule.logic(60.0 / self.tempo / self.grid)
+                            break
+                        else:
+                            break
+                    except KeyboardInterrupt:
+                        # log(FG.RED + traceback.format_exc())
+                        self.quitflag = True
+                        break
+                    except:
+                        log(FG.RED + traceback.format_exc())
+                        if self.shell:
+                            self.quitflag = True
+                            break
+                        if not self.shell and not 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:
+                    self.quitflag = True
+                    break
+                if not self.shell and not self.pause():
+                    break
+
+            self.row += 1
+
diff --git a/src/support.py b/src/support.py
index 60eb0c5..7d10a18 100644
--- a/src/support.py
+++ b/src/support.py
@@ -1,11 +1,11 @@
 from . import get_args
 ARGS = get_args()
 SUPPORT = set(['midi'])
-SUPPORT_ALL = set(['sonic-pi','csound','midi']) # gme,mpe
+SUPPORT_ALL = set(['supercollider','csound','midi']) # gme,mpe
 psonic = None
-if ARGS['--sonic-pi']:
-    import psonic
-    SUPPORT.add('sonic-pi')
+if ARGS['--supercollider']:
+    import osc
+    SUPPORT.add('supercollider')
 
 csound = None
 if ARGS['--csound']:
diff --git a/src/theory.py b/src/theory.py
index a883085..26c372c 100644
--- a/src/theory.py
+++ b/src/theory.py
@@ -3,11 +3,20 @@
 from future.utils import iteritems
 from collections import OrderedDict
 from . import get_defs
+from .parser import *
 
 FLATS=False
 SOLFEGE=False
 NOTENAMES=True # show note names instead of numbers
 
+class NoSuchScale(BaseException):
+    pass
+class NoSuchNote(BaseException):
+    pass
+
+NOTE_OFFSET_VALUES = [None,1,None,2,None,3,4,None,5,None,6,None,7]
+# LETTER_OFFSET_VALUES = [None,'C',None,'D',None,'E','F',None,'G',None,'A',None,'B']
+
 SOLFEGE_NOTES ={
     'do': '1',
     'di': '#1',
@@ -175,3 +184,29 @@ def expand_chord(c):
         c = c[:-1] + 'm'
     return CHORDS[normalize_chord(c)].split(' ')
 
+ALIGNED_NOTE_NAMES = [
+    ['1','b2','2','b3','3','4','b5','5','b6','6','b7','7'],
+    ['','#1','','#2','','','#4','','#5','','#6',''],
+    ['C','Db','D','Eb','E','F','Gb','G','Ab','A','Bb','B'],
+    ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'],
+    ['bb2','bbb3','bb3','bb4','b4','bb5','bbb6','bb6','bbb7','bb7',''],
+    ['','','###1','##2','###2','##3','##4','###4','##5','###5','##6',''],
+    ['Dbb','Ebbb','Ebb','Fbb','Fb','Gbb','Abbb','Abb','Bbbb','Bbb',''],
+    ['','','C###','D##','D###','E##','F##','F###','G##','G###','A##','']
+]
+
+def note_offset(s):
+    n = 0
+    sharps = count_seq(s,'#')
+    n += sharps
+    flats = count_seq(s,'b')
+    n -= flats
+    s = s[:sharps + flats]
+    if s:
+        for names in ALIGNED_NOTE_NAMES:
+            try:
+                return n + names.index(s)
+            except ValueError:
+                pass
+    raise NoSuchNote()
+
diff --git a/src/track.py b/src/track.py
index b89cc5d..9829bc0 100644
--- a/src/track.py
+++ b/src/track.py
@@ -1,13 +1,18 @@
 from . import *
 
+class Recording:
+    def __init__(self, name, slot):
+        self.name = slot
+        self.content = []
+
 class Track:
     FLAGS = set('auto_roman')
-    def __init__(self, ctx, idx, midich, player, schedule):
+    def __init__(self, ctx, idx, midich):
         self.idx = idx
         self.ctx = ctx
         # self.players = [player]
-        self.player = player
-        self.schedule = schedule
+        self.player = ctx.player
+        self.schedule = ctx.schedule
         self.channels = [midich]
         self.midich = midich # tracks primary midi channel
         self.initial_channel = midich
@@ -25,6 +30,7 @@ def reset(self):
         self.sustain = False # sustain everything?
         self.arp_notes = [] # list of notes to arpegiate
         self.arp_idx = 0
+        self.arp_notes_left = 0
         self.arp_cycle_limit = 0 # cycles remaining, only if limit != 0
         self.arp_pattern = [] # relative steps to
         self.arp_enabled = False
@@ -55,6 +61,10 @@ def reset(self):
         self.enabled = True
         self.soloed = False
         self.volval = 1.0
+        self.slots = {} # slot -> 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<<x,x) for x in range(16)]), lambda x: x[1]))
@@ -63,9 +73,11 @@ def volume(self,v=None):
             return self.volval
         self.volval = v
         self.cc(7,int(v*127.0))
+        self.ccs[1] = v
     def refresh(self):
         self.cc(1,0)
         self.volume(self.volval)
+        self.ccs[7] = v
     def add_flags(self, f):
         if f != f & FLAGS:
             raise ParseError('invalid flags')
@@ -111,6 +123,10 @@ 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:
                 self.player.note_on(n,v,ch)
+                if self.ctx.midifile:
+                    self.ctx.midifile.tracks[ch].append(mido.Message(
+                        'note_on',velocity=v,time=self.ctx.t,channel=ch
+                    ))
     def note_off(self, n, v=-1):
         if v == -1:
             v = self.vel
@@ -177,6 +193,7 @@ def cc(self, cc, val): # control change
             status = (MIDI_CC<<4) + ch
             if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status, cc,val))
             self.player.write_short(status,cc,val)
+            self.ccs[cc] = v
         if cc==1:
             self.modval = val
         if cc==7:
@@ -240,6 +257,7 @@ def arp(self, notes, count=0, sustain=False, pattern=[1], reverse=False):
         self.arp_cycle = count
         self.arp_pattern = pattern
         self.arp_pattern_idx = 0
+        self.arp_notes_left = len(notes) * count
         self.arp_idx = 0 # use inversions to move this start point (?)
         self.arp_once = False
         self.arp_sustain = False
@@ -247,8 +265,13 @@ def arp_stop(self):
         self.arp_enabled = False
         self.release_all()
     def arp_next(self):
+        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?
             self.arp_once = True
             if self.arp_cycle_limit:
@@ -280,4 +303,3 @@ def tuplet_stop(self):
         self.note_spacing = 1.0
         self.tuplet_offset = 0.0
 
-
diff --git a/test/run.dc b/test/run.dc
index 547ecfd..43135b5 100644
--- a/test/run.dc
+++ b/test/run.dc
@@ -1,24 +1,24 @@
-%t120x2 p=piano,piano
+%t120x2 p=piano,drums,bass c20,-2
 
-sus2$__
+dim$__             1                   b3,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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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=<fn> | --ring | --follow | --csound | --supercollider] [-eftnpsrxh] [SONGNAME]
-    decadence.py [+RANGE] [--ring || --follow | --csound | --supercollider] [-eftnpsrxh] [SONGNAME]
+    decadence.py [--dev=<device> | --verbose | --midi=<fn> | --ring | --follow | --csound | --supercollider] [-eftnpsrxhv] [SONGNAME]
+    decadence.py [+RANGE] [--dev=<device> | --midi=<fn> | --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=<bpm>      (STUB) set tempo
-    -x --grid=<g>         (STUB) set grid
-    -n --note=<n>         (STUB) set grid using note value
-    -s --speed=<s>        (STUB) playback speed
+    -t --tempo=<bpm>      (STUB) set tempo [default: 120]
+    -x --grid=<g>         (STUB) set grid [default: 4]
+    -n --note=<n>         (STUB) set grid using note value [default: 1]
+    -s --speed=<s>        (STUB) playback speed [speed: 1.0]
     --dev=<device>        output device, partial match
     -p --patch=<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=<fn>           generate midi file
     +<range>              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<<x,x) for x in range(16)]), lambda x: x[1]))
+    def master(self):
+        return self
+    def get_track(self):
+        return self.lanes[self.lane] if self.lane!=None else self
+    # def get_lane(self):
+    #     return self.lanes[self.lane] if self.lanes else lane
     def volume(self,v=None):
         if v==None:
             return self.volval
@@ -90,6 +117,7 @@ def enable(self, v=True):
     def disable(self, v=True):
         self.enable(not v)
     def stop(self):
+        self.release_all(True)
         for ch in self.channels:
             status = (MIDI_CC<<4) + ch
             if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status,120,0))
@@ -98,6 +126,7 @@ def stop(self):
                 self.refresh()
                 self.modval = False
     def panic(self):
+        self.release_all(True)
         for ch in self.channels:
             status = (MIDI_CC<<4) + ch
             if self.ctx.showmidi: log(FG.YELLOW + 'MIDI: CC (%s, %s, %s)' % (status,123,0))
@@ -136,7 +165,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_off(n,v,ch)
+                self.player.note_on(self.notes[n],0,ch)
+                self.player.note_off(self.notes[n],v,ch)
                 self.notes[n] = 0
                 self.sustain_notes[n] = 0
             self.cc(MIDI_SUSTAIN_PEDAL, True)
@@ -151,6 +181,7 @@ 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.notes[n] = 0
                     self.sustain_notes[n] = 0
@@ -159,7 +190,7 @@ def release_all(self, mute_sus=False, v=-1):
         if self.modval>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 <flipcoder@gmail.com>
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=<device> | --verbose | --midi=<fn> | --ring | --follow | --csound | --supercollider] [-eftnpsrxhv] [SONGNAME]
-    decadence.py [+RANGE] [--dev=<device> | --midi=<fn> | --ring | --follow | --csound | --supercollider] [-eftnpsrxhv] [SONGNAME]
+    decadence.py [--dev=<device> | --verbose | --midi=<fn> | --ring | --follow | --csound | --supercollider | --loop] [-eftnpsrxhv] [SONGNAME]
+    decadence.py [+RANGE] [--dev=<device> | --midi=<fn> | --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=<fn>           generate midi file
     +<range>              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<b)
 def sgn(a):
diff --git a/src/player.py b/src/player.py
index bb068f0..13b2830 100644
--- a/src/player.py
+++ b/src/player.py
@@ -13,6 +13,22 @@ def __init__(self, row, caller, count):
         # self.returns[row] = 0
 
 class Player:
+    
+    class Flag:
+        ROMAN = bit(0)
+        TRANSPOSE = bit(1)
+        LOOP = bit(2)
+    FLAGS = [
+        'roman',
+        'transpose',
+        'loop'
+    ]
+    # FLAGS = set([
+    #     'roman', # STUB: fit roman chord in scale shape
+    #     'transpose', # allow transposition of note letters
+    #     'loop'
+    # ])
+
     def __init__(self):
         self.quitflag = False
         self.vimode = False
@@ -62,6 +78,35 @@ def __init__(self):
         self.last_follow = 0
         self.last_marker = -1
         self.midifile = None
+        self.flags = 0
+
+    def add_flags(self, f):
+        if isinstance(f, basestring):
+            f = 1 << self.FLAGS.index(f)
+        elif isinstance(f, int):
+            assert f > 0
+        else:
+            for e in f:
+                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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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=<device> | --verbose | --midi=<fn> | --ring | --follow | --csound | --supercollider | --loop] [-eftnpsrxhv] [SONGNAME]
-    decadence.py [+RANGE] [--dev=<device> | --midi=<fn> | --ring | --follow | --csound | --supercollider | --loop] [-eftnpsrxhv] [SONGNAME]
+    decadence.py [--dev=<device> | --verbose | --midi=<fn> | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME]
+    decadence.py [+RANGE] [--dev=<device> | --midi=<fn> | --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<<x,x) for x in range(16)]), lambda x: x[1]))
+    def device(self, dev, stackidx=-1):
+        if stackidx == -1:
+            self.dev = dev
+            for ch in self.channels:
+                self.device(dev,ch)
+            return
+        ch = self.channels[stackidx]
+        self.channels[stackidx] = (dev,ch[1])
     def master(self):
         return self
     def get_track(self):
@@ -134,18 +143,18 @@ def disable(self, v=True):
     def stop(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,120,0))
-            self.player.write_short(status, 120, 0)
+            self.midi[ch[0]].write_short(status, 120, 0)
             if self.modval>0:
                 self.refresh()
                 self.modval = False
     def panic(self):
         self.release_all(True)
         for ch in self.channels:
-            status = (MIDI_CC<<4) + ch
+            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 <flipcoder@gmail.com>
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 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE CARLA-PROJECT>
+<CARLA-PROJECT VERSION='2.0'>
+ <EngineSettings>
+  <ForceStereo>true</ForceStereo>
+  <PreferPluginBridges>false</PreferPluginBridges>
+  <PreferUiBridges>true</PreferUiBridges>
+  <UIsAlwaysOnTop>false</UIsAlwaysOnTop>
+  <MaxParameters>200</MaxParameters>
+  <UIBridgesTimeout>4000</UIBridgesTimeout>
+ </EngineSettings>
+
+ 
+
+ <ExternalPatchbay>
+  <Connection>
+   <Source>Carla:AudioOut1</Source>
+   <Target>AudioOut:playback_1</Target>
+  </Connection>
+  <Connection>
+   <Source>Carla:AudioOut1</Source>
+   <Target>AudioOut:playback_1</Target>
+  </Connection>
+  <Connection>
+   <Source>Carla:AudioOut2</Source>
+   <Target>AudioOut:playback_2</Target>
+  </Connection>
+  <Connection>
+   <Source>Carla:AudioOut2</Source>
+   <Target>AudioOut:playback_2</Target>
+  </Connection>
+  <Connection>
+   <Source>MidiIn:Virtual Raw MIDI 1-0:VirMIDI 1-0 20:0</Source>
+   <Target>Carla:MidiIn</Target>
+  </Connection>
+ </ExternalPatchbay>
+</CARLA-PROJECT>
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 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE CARLA-PROJECT>
+<CARLA-PROJECT VERSION='2.0'>
+ <EngineSettings>
+  <ForceStereo>true</ForceStereo>
+  <PreferPluginBridges>false</PreferPluginBridges>
+  <PreferUiBridges>true</PreferUiBridges>
+  <UIsAlwaysOnTop>false</UIsAlwaysOnTop>
+  <MaxParameters>200</MaxParameters>
+  <UIBridgesTimeout>4000</UIBridgesTimeout>
+ </EngineSettings>
+
+ <Plugin>
+  <Info>
+   <Type>LV2</Type>
+   <Name>amsynth</Name>
+   <URI>http://code.google.com/p/amsynth/amsynth</URI>
+  </Info>
+
+  <Data>
+   <Active>Yes</Active>
+   <ControlChannel>N</ControlChannel>
+   <Options>0x0</Options>
+  </Data>
+ </Plugin>
+
+ <Plugin>
+  <Info>
+   <Type>LV2</Type>
+   <Name>Helm</Name>
+   <URI>http://tytel.org/helm</URI>
+  </Info>
+
+  <Data>
+   <Active>Yes</Active>
+   <ControlChannel>N</ControlChannel>
+   <Options>0x1</Options>
+  </Data>
+ </Plugin>
+
+ <ExternalPatchbay>
+  <Connection>
+   <Source>Carla:AudioOut1</Source>
+   <Target>AudioOut:playback_1</Target>
+  </Connection>
+  <Connection>
+   <Source>Carla:AudioOut1</Source>
+   <Target>AudioOut:playback_1</Target>
+  </Connection>
+  <Connection>
+   <Source>Carla:AudioOut2</Source>
+   <Target>AudioOut:playback_2</Target>
+  </Connection>
+  <Connection>
+   <Source>Carla:AudioOut2</Source>
+   <Target>AudioOut:playback_2</Target>
+  </Connection>
+  <Connection>
+   <Source>MidiIn:Virtual Raw MIDI 1-0:VirMIDI 1-0 20:0</Source>
+   <Target>Carla:MidiIn</Target>
+  </Connection>
+ </ExternalPatchbay>
+</CARLA-PROJECT>
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 <flipcoder@gmail.com>
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
diff --git a/src/__init__.py b/src/__init__.py
index d62dd67..1b5824d 100644
--- a/src/__init__.py
+++ b/src/__init__.py
@@ -92,10 +92,10 @@ def constrain(a,n1=1,n2=0):
             n1 = 1.0
     return min(max(n1,n2),max(min(n1,n2),a))
 
-APPNAME = 'decadence'
+APPNAME = 'textbeat'
 DIR = appdirs.AppDirs(APPNAME)
 # LOG_FN = os.path.join(DIR.user_log_dir,'.log')
-HISTORY_FN = os.path.join(DIR.user_config_dir, '.history')
+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__,'..')))
 CFG_PATH = os.path.join(SCRIPT_PATH, 'config')
diff --git a/src/player.py b/src/player.py
index 4bf1794..e6212ea 100644
--- a/src/player.py
+++ b/src/player.py
@@ -62,7 +62,7 @@ def __init__(self):
         # self.rowno = []
         self.startrow = -1
         self.stoprow = -1
-        self.dcmode = 'n' # n normal c command s sequence
+        self.cmdmode = 'n' # n normal c command s sequence
         self.schedule = Schedule(self)
         self.tracks = []
         self.shell = False
@@ -80,12 +80,63 @@ def __init__(self):
         self.midifile = None
         self.flags = 0
         self.version = '0'
+        self.auto = False
+        self.embedded_files = {}
 
         # require enable at top of file
         self.devices = ['midi']
         
-        self.renderman = False
+    def init(self):
         
+        for i in range(len(sys.argv)):
+            arg = sys.argv[i]
+
+            # play range (+ param, comma-separated start and end)
+            if arg.startswith('+'):
+                vals = arg[1:].split(',')
+                try:
+                    self.startrow = int(vals[0])
+                except ValueError:
+                    try:
+                        self.startrow = self.markers[vals[0]]
+                    except KeyError:
+                        log('invalid entry point')
+                        self.quitflag = True
+                try:
+                    self.stoprow = int(vals[1])
+                except ValueError:
+                    try:
+                        # we cannot cut buf now, since seq might be non-linear
+                        self.stoprow = self.markers[vals[0]]
+                    except KeyError:
+                        log('invalid stop point')
+                        self.quitflag = True
+                except IndexError:
+                    pass # no stop param
+        
+        # read embedded files
+        for row in self.buf:
+            embedded_fn = None
+            embedded_file = []
+            base_indent = ''
+            if embedded_fn:
+                if row.startswith(' ') or row.startswith('\t'):
+                    if not base_indent:
+                        base_indent = row[0] * count_seq(row)
+                        embedded_file = self.row
+                    embedded_file += [row[base_indent:]]
+                else:
+                    base_indent = ''
+                    self.embedded_files[embedded_fn] = embedded_file
+                    embedded_fn = None
+                    embedded_file = []
+            
+            if row.startswith('`'):
+                embedded_fn = row[1:]
+                embedded_file = []
+            
+        # print(self.embedded_files.keys())
+
     def refresh_devices(self):
         # determine output device support and load external programs
         from . import support
@@ -94,10 +145,15 @@ def refresh_devices(self):
                 print('device not supported by system: ' + dev)
                 assert False
             try:
-                support.support_init[dev]()
+                support.support_init[dev](self.rack)
             except KeyError:
                 # no init needed, silent
                 pass
+        self.auto = 'auto' in self.devices
+
+    def set_rack(self, plugins):
+        self.rack = plugins
+        self.refresh_devices()
         
     def add_flags(self, f):
         if isinstance(f, basestring):
@@ -150,6 +206,7 @@ def run(self):
             ch.refresh()
         
         self.header = True
+        embedded_file = False
         
         while not self.quitflag:
             self.follow()
@@ -172,13 +229,13 @@ def run(self):
                     # done with file, finish playing some stuff
                     
                     arps_remaining = 0
-                    if self.interactive or self.dcmode in ['c','l']: # finish arps in shell mode
+                    if self.interactive or self.cmdmode in ['c','l']: # finish arps in shell mode
                         for ch in self.tracks[:self.tracks_active]:
                             if ch.arp_enabled:
                                 if ch.arp_cycle_limit or not ch.arp_once:
                                     arps_remaining += 1
                                     self.line = '.'
-                        if not arps_remaining and not self.shell and self.dcmode not in ['c','l']:
+                        if not arps_remaining and not self.shell and self.cmdmode not in ['c','l']:
                             break
                         self.line = '.'
                     
@@ -191,15 +248,15 @@ def run(self):
                                 # 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))+' '+\
+                                # 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=<device> | --verbose | --midi=<fn> | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME]
-    decadence.py [+RANGE] [--dev=<device> | --midi=<fn> | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME]
-    decadence.py -c [COMMANDS ...]
-    decadence.py -l [LINE_CONTENT ...]
+    textbeat.py [--dev=<device> | --verbose | --midi=<fn> | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME]
+    textbeat.py [+RANGE] [--dev=<device> | --midi=<fn> | --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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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
diff --git a/test/cc.txbt b/test/cc.txbt
index 0ffe953..f2cbfc2 100644
--- a/test/cc.txbt
+++ b/test/cc.txbt
@@ -1,4 +1,3 @@
-; run this using Helm synth
 %f=loop c15,-2
 1@bs0:0
 b3
diff --git a/test/modes.txbt b/test/modes.txbt
index b8b2005..df6d436 100644
--- a/test/modes.txbt
+++ b/test/modes.txbt
@@ -14,7 +14,7 @@
 5
 6
 7
-%r2
+%k2
 1
 2
 3
diff --git a/test/organ.txbt b/test/organ.txbt
deleted file mode 100644
index b55c069..0000000
--- a/test/organ.txbt
+++ /dev/null
@@ -1,37 +0,0 @@
-%t90
-%g4
-%ppiano,piano,drums
-
-maj9/1/1       .              1
-                 
-                              b5
-
-                              3
-
-                              b5
-                              1) 
-                              1
-
-                              b5
-
-                              3
-
-                              b5
-                              1) 
-4maj9/4/4      .              1  
-                 
-                              b5
-
-                              3
-
-                              b5
-                              1) 
-                              1
-
-                              b5
-
-                              3
-
-                              b5
-                              1)
-                              1
diff --git a/textbeat.py b/textbeat.py
deleted file mode 100755
index 224b7eb..0000000
--- a/textbeat.py
+++ /dev/null
@@ -1,291 +0,0 @@
-#!/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=<device> | --verbose | --midi=<fn> | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME]
-    textbeat.py [+RANGE] [--dev=<device> | --midi=<fn> | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME]
-    textbeat.py -c [COMMANDS ...]
-    textbeat.py -l [LINE_CONTENT ...]
-
-Options:
-    -h --help             show this
-    -v --verbose          verbose
-    -t --tempo=<bpm>      (STUB) set tempo [default: 120]
-    -x --grid=<g>         (STUB) set grid [default: 4]
-    -n --note=<n>         (STUB) set grid using note value [default: 1]
-    -s --speed=<s>        (STUB) playback speed [speed: 1.0]
-    --dev=<device>        output device, partial match
-    -p --patch=<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=<fn>           generate midi file
-    +<range>              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=<device> | --verbose | --midi=<fn> | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME]
+    textbeat.py [+RANGE] [--dev=<device> | --midi=<fn> | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME]
+    textbeat.py -c [COMMANDS ...]
+    textbeat.py -l [LINE_CONTENT ...]
+
+Options:
+    -h --help             show this
+    -v --verbose          verbose
+    -t --tempo=<bpm>      (STUB) set tempo [default: 120]
+    -x --grid=<g>         (STUB) set grid [default: 4]
+    -n --note=<n>         (STUB) set grid using note value [default: 1]
+    -s --speed=<s>        (STUB) playback speed [speed: 1.0]
+    --dev=<device>        output device, partial match
+    -p --patch=<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=<fn>           generate midi file
+    +<range>              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<<x,x) for x in range(16)]), lambda x: x[1]))
-    def device(self, dev, stackidx=-1):
+    def get_device(self):
+        return self.ctx.devices[self.dev]
+    def set_device(self, dev, stackidx=-1):
         if stackidx == -1:
             self.dev = dev
             for ch in self.channels:
diff --git a/txbt b/txbt
new file mode 100755
index 0000000..7e36d90
--- /dev/null
+++ b/txbt
@@ -0,0 +1,2 @@
+#!/bin/bash
+PYTHONPATH=`dirname $0` python -m textbeat "$*"

From b3d4c0c7b1a0e6bc1f3c74c9960a3e8b45768937 Mon Sep 17 00:00:00 2001
From: Grady O'Connell <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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=<device> | --verbose | --midi=<fn> | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME]
-    textbeat.py [+RANGE] [--dev=<device> | --midi=<fn> | --ring | --follow | --loop] [-eftnpsrxhv] [SONGNAME]
-    textbeat.py -c [COMMANDS ...]
-    textbeat.py -l [LINE_CONTENT ...]
+    textbeat [--dev=<device> | --midi=<fn> | --ring | --follow --stdin] [-aeftnpsrxhvL] [SONGNAME]
+    textbeat [+RANGE] [--dev=<device> | --midi=<fn> | --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=<bpm>      (STUB) set tempo [default: 120]
     -x --grid=<g>         (STUB) set grid [default: 4]
     -n --note=<n>         (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=<fn>           generate midi file
     +<range>              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<EPSILON):
                             ch.note_on(p + n, vel, sustain)
                         else:
-                            f = lambda _,ch=ch,p=p,n=n,vel=vel,sustain=sustain: ch.note_on(p + n, vel, sustain)
+                            f = lambda _, p=p,n=n,s=sustain,v=vel: \
+                                ch.note_on(p + n, v, s)
                             self.schedule.add(Event(delay,f,ch))
                         delay += delta
                         i += 1
diff --git a/textbeat/plugins/synth.py b/textbeat/plugins/synth.py
new file mode 100755
index 0000000..d856945
--- /dev/null
+++ b/textbeat/plugins/synth.py
@@ -0,0 +1,176 @@
+#!/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
+
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 += '<!--'+name+'-->\n'+\
+                    '<Plugin><Info>\n'+\
+                    '<Type>'+ext+'</Type>\n'+\
+                    '<Name>'+name+'</Name>\n'+\
+                    '<URI>x</URI>'+\
+                    '</Info>\n'+\
+                    '<Data>\n'+\
+                        '<ControlChannel>N</ControlChannel>\n'+\
+                        '<Active>Yes</Active>\n'+\
+                        '<Options>'+hex(i)+'</Options>\n'+\
+                    '</Data>'+\
+                    '</Plugin>\n\n'
+                i += 1
+            filebuf = filebuf.replace('</EngineSettings>', '</EngineSettings>'+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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <idbrii@gmail.com>
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 <module>
    from .defs import *
  File "E:\clones\textbeat\textbeat\defs.py", line 10, in <module>
    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 <flipcoder@gmail.com>
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=<device> | --midi=<fn> | --ring | --follow --stdin] [-aeftnpsrxhvL] [SONGNAME]
-    textbeat [+RANGE] [--dev=<device> | --midi=<fn> | --ring | --follow | --stdin] [-aeftnpsrxhvL] [SONGNAME]
+    textbeat [--dev=<device>] [--midi=<fn>] [--ring] [--follow] [--stdin] [-adeftnpsrxhvL] [INPUT]
+    textbeat [+RANGE] [--dev=<device> | --midi=<fn> | --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=<fn>           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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 += '<!--'+name+'-->\n'+\
+                        '<Plugin><Info>\n'+\
+                        '<Type>'+ext+'</Type>\n'+\
+                        '<Name>'+name+'</Name>\n'+\
+                        '<URI>x</URI>'+\
+                        '</Info>\n'+\
+                        '<Data>\n'+\
+                            '<ControlChannel>N</ControlChannel>\n'+\
+                            '<Active>Yes</Active>\n'+\
+                            '<Options>'+hex(i)+'</Options>\n'+\
+                        '</Data>'+\
+                        '</Plugin>\n\n'
+                    i += 1
+                filebuf = filebuf.replace('</EngineSettings>', '</EngineSettings>'+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 += '<!--'+name+'-->\n'+\
-                    '<Plugin><Info>\n'+\
-                    '<Type>'+ext+'</Type>\n'+\
-                    '<Name>'+name+'</Name>\n'+\
-                    '<URI>x</URI>'+\
-                    '</Info>\n'+\
-                    '<Data>\n'+\
-                        '<ControlChannel>N</ControlChannel>\n'+\
-                        '<Active>Yes</Active>\n'+\
-                        '<Options>'+hex(i)+'</Options>\n'+\
-                    '</Data>'+\
-                    '</Plugin>\n\n'
-                i += 1
-            filebuf = filebuf.replace('</EngineSettings>', '</EngineSettings>'+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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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=<device>] [--midi=<fn>] [--ring] [--follow] [--stdin] [-adeftnpsrxhvL] [INPUT]
-    textbeat [+RANGE] [--dev=<device> | --midi=<fn> | --ring | --follow | --stdin] [-adeftnpsrxhvL] [INPUT]
-    textbeat [-rT]
+    textbeat [--dev=<device>] [--midi=<fn>] [--ring] [--follow] [--stdin] [-adeftnpsrxvL] [INPUT]
+    textbeat [+RANGE] [--dev=<device>] [--midi=<fn>] [--ring] [--follow] [--stdin] [-adeftnpsrxL] [INPUT]
+    textbeat [-rhT]
     textbeat -c [COMMANDS ...]
     textbeat -l [LINE_CONTENT ...]
 
@@ -36,7 +36,7 @@
     +<range>              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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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<EPSILON):
                             ch.note_on(p + n, vel, sustain)
                         else:
+                            # schedule note to occur later
                             f = lambda _, p=p,n=n,s=sustain,v=vel: \
                                 ch.note_on(p + n, v, s)
                             self.schedule.add(Event(delay,f,ch))
@@ -1776,6 +1815,7 @@ def run(self):
          
                 while True:
                     try:
+                        # don't delay on ctrl lines or file header
                         if not ctrl and not self.header:
                             self.schedule.logic(60.0 / self.tempo / self.grid)
                             break

From 2f12b882fc08d911dda907c6e437302c4302d5e6 Mon Sep 17 00:00:00 2001
From: Grady O'Connell <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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 <flipcoder@gmail.com>
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=<device>] [--midi=<fn>] [--ring] [--follow] [--stdin] [-adeftnpsrxvL] [INPUT]
-    textbeat [+RANGE] [--dev=<device>] [--midi=<fn>] [--ring] [--follow] [--stdin] [-adeftnpsrxL] [INPUT]
+    textbeat [--dev=<device>] [--midi=<fn>] [--ring] [--follow] [--stdin] [--vi] [-adeftnpsrxvL] [INPUT]
+    textbeat [+RANGE] [--dev=<device>] [--midi=<fn>] [--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 <callum.cassidynolan@mail.utoronto.ca>
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 <your file>
+```
+
+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 <callum.cassidynolan@mail.utoronto.ca>
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 <callum.cassidynolan@mail.utoronto.ca>
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 <flipcoder@gmail.com>
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 <kianmeng@cpan.org>
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=<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']
