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