Here is a working version of the script proposed in the last post. I think I've put everything you need to know into the help. Works nicely on my vintage 2008 white Macbook under OS X 10.6.7. Drift (displayed at end of run) is typically 30 ~ 40 msec in 32 seconds of interaction (16 bars at 4=120). That's close enough for my purposes, but I think it could be tightened up with a more sophisticated calculation of the sleep time after each beat -- or possibly by using callbacks instead of a simple loop.
In using it, I found it helpful to set my terminal window to a large font to make the counts easier to see from several feet away.
Feedback and suggestions for improvement welcome.
Enjoy,
Mike
Code: Select all
#!/usr/bin/env python
"""
Author: Michael Ellis
Copyright 2011 Ellis & Grant, Inc.
License: GPL2, Warranty: Absolutely none.
OSC control interface to SooperLooper for alternating record/listen.
Supports multiple measure loops with changes of meter and tempo.
Requirements:
1. This program uses the python argparse module which is new in
python 2.7.
2. You need to have a working instance of the SooperLooper engine
running and connected to your desired inputs and outputs before
running this script.
3. You also need liblo and pyliblo installed.
For more information about SooperLooper, visit
http://www.essej.net/sooperlooper/
"""
import sys
import os
import argparse
import time
import liblo
_usage_examples = """
EXAMPLES
slpractice.py (no arguments)
Performs a two bar count-in (4 beats per bar) followed by
8 record/listen cycles of 1 bar at 120 beats per minute.
Terminal output appears as shown below with each count printed
in correct rhythm, ie as a visual metronome.
COUNT-IN
1 2 3 4
1 2 3 4
PLAY
1 2 3 4
LISTEN
1 2 3 4
PLAY
1 2 3 4
LISTEN
1 2 3 4
<< 6 more cycles >>
slpractice.py 4 4*90 4
Performs cycles of 3 bars with a 10% ritard in the second bar
and an 'a tempo' in the third. As in the previous example
there is a two bar count-in followed by 8 record/listen cycles.
Baseline tempo is 120 beats per minute.
slpractice.py -c1 -t90 -r4 2 3*150
Performs a 1 bar count_in of two beats at 90 bpm, then
4 cycles of a 2-beat bar at 90 bpm and a 3-beat bar at 135 bpm.
Note that this example is contrived so that both measures have
the same duration (as if changing between 6/8 and 3/4 time).
The same result could be obtained in other ways, including:
slpractice.py -c1 -t90 -r4 2 3@135
slpractice.py -c1 -t135 -r4 2*66.7 3
"""
## ----------------------------------------------------------------------------
## Command line parser
## ----------------------------------------------------------------------------
_parser = argparse.ArgumentParser(description=__doc__,
epilog = _usage_examples,
formatter_class=argparse.RawDescriptionHelpFormatter)
_parser.add_argument('measures', metavar='BARSPEC', type=str, nargs='*',
default="4",
help='The number of beats in the bar optionally '\
'followed by a relative or absolute tempo indicator '\
'e.g. 4*90 means 4 beats at 90%% of the baseline '\
'tempo whereas 4@90 means 4 beats at 90 bpm. '\
'Tempo reverts to baseline at the end of each bar.')
_parser.add_argument('-t','--tempo', type = int, action = 'store',
default = 120,
help = "baseline tempo in beats per minute "\
"(default: 120)")
_parser.add_argument('-r','--repeats', type = int, action = 'store',
default = 8,
help = 'number of repetitions of the '\
'record/listen cycle (default: 8)')
_parser.add_argument('-c','--count_in', type = int, action = 'store',
default = 2,
help = 'number of measures for the initial count-in. '\
'(default: 2). Tempo and meter for count-in '\
'measures are taken from the first BARSPEC')
_parser.add_argument('-p','--osc-port', type = int, action = 'store',
default = 9951,
help = 'Port number on which SooperLooper, (SL), is listening. ' \
'(default: 9951) The default is the standard port ' \
'when operating standalone.')
def parse_barspec(s, args):
"""
Handle one barspec. If it is valid, return a tuple of (beat, tempo),
otherwise print the barspec and raise SystemExit.
"""
try:
if '*' in s:
beats,relative_tempo = s.split('*')
tempo = float(relative_tempo) * args.tempo / 100.
elif '@' in s:
beats, tempo = s.split('@')
tempo = float(tempo)
else:
beats = s
tempo = float(args.tempo)
beats = int(beats)
except ValueError:
print "Bad BARSPEC: %s" % s
raise SystemExit
return beats,tempo
## ----------------------------------------------------------------------------
## Utility Functions
## ----------------------------------------------------------------------------
def count_bar(beats, tempo):
""" Visual metronome for terminal display. """
beat_length = 60.0/tempo
lis = ["%d" % (i+1) for i in xrange(beats)]
# print " ".join(lis)
for n in lis:
sys.stdout.write("%s " % n)
sys.stdout.flush()
time.sleep(beat_length)
sys.stdout.write("\n")
def run():
"""
The 'main' function for this script. Parses the command line args,
creates the list of measure specs, displays count-in, then enters
the play/listen loop for the number of repeats requested on the
command line.
Side effects:
Changes state of SooperLooper server including overwriting contents
of loop 0.
"""
args = _parser.parse_args()
target = liblo.Address(args.osc_port)
allbars = []
for bar in args.measures:
allbars.append(parse_barspec(bar, args))
expected_duration = 2 * args.repeats * sum([b[0]*60./b[1] for b in allbars])
total_bars = 2 * args.repeats * len(allbars)
#TODO init, including stop any playback or recording.
print "COUNT-IN"
for i in xrange(args.count_in):
count_bar(*allbars[0])
start = liblo.time()
for r in xrange(args.repeats):
rno = r + 1
# Start recording
liblo.send(target, '/sl/0/hit', 'record')
print "\n%d. PLAY" % rno
for bar in allbars:
count_bar(*bar)
# At end of last beat, start playback in 'once' mode
liblo.send(target, '/sl/0/hit', 'oneshot')
print "\n%d. LISTEN" % rno
for bar in allbars:
count_bar(*bar)
#TODO any necessary cleanup before exiting.
end = liblo.time()
actual_duration = end - start
print
print "Expected duration: %.3f s." % expected_duration
print "Actual duration: %.3f s." % actual_duration
print "Timing drift = %0.2f seconds in %d bars" % \
(actual_duration - expected_duration, total_bars)
## ----------------------------------------------------------------------------
if __name__ == '__main__':
run()