sched
library,
which support precise timing and scheduling, as well as some
MIDI-related libraries that can help you select devices, parse
incoming messages, and form outgoing messages.We're going to look at implementations and libraries as follows:
Recently, Serpent has been extended with co-routine-like threads. All of the scheduling examples in this document have been implemented using Serpent threads, and these examples are described on this page on using threads.
Notes:
require "debug" require "sched" def activity(i): display "activity", i, sched_rtime, time_get() sched_cause(1, nil, 'activity', i + 1) sched_init() sched_cause(1, nil, 'activity', 101) sched_run()
Notice that the ideal event times, accessed as sched_rtime are precisely 1 second apart, as if the computer is infinitely fast and time is not quantized. The actual time, accessed as time_get() is close to the ideal time, but sometimes a millisecond or two late.
activity: i = 101, sched_rtime = 1, time_get() = 1 activity: i = 102, sched_rtime = 2, time_get() = 2.002 activity: i = 103, sched_rtime = 3, time_get() = 3.001 activity: i = 104, sched_rtime = 4, time_get() = 4.001 activity: i = 105, sched_rtime = 5, time_get() = 5 activity: i = 106, sched_rtime = 6, time_get() = 6 activity: i = 107, sched_rtime = 7, time_get() = 7.001 activity: i = 108, sched_rtime = 8, time_get() = 8 activity: i = 109, sched_rtime = 9, time_get() = 9.001 activity: i = 110, sched_rtime = 10, time_get() = 10.001 ^C
Here is some output generated by the program. Notice that when 2 events are scheduled for the same time, they run sequentially, but it is not obvious which will run first. (In this case, ordering is deterministic, but order depends on the implementation of the priority queue in the scheduler.)
require "debug" require "sched" def activity_1(i): display "act1 ", i, sched_rtime, time_get() sched_cause(1, nil, 'activity_1', i + 1) def activity_2(i): display " act2", i, sched_rtime, time_get() sched_cause(3, nil, 'activity_2', i + 1) sched_init() sched_cause(1, nil, 'activity_1', 101) sched_cause(1, nil, 'activity_2', 201) sched_run()
act1
”) runs every second,
while activity 2 (“act2
”) runs every 3 seconds.
act2: i = 201, the_sched.time = 1, time_get() = 1.001 act1 : i = 101, sched_rtime = 1, time_get() = 1.001 act1 : i = 102, sched_rtime = 2, time_get() = 2 act1 : i = 103, sched_rtime = 3, time_get() = 3.001 act1 : i = 104, sched_rtime = 4, time_get() = 4.001 act2: i = 202, sched_rtime = 4, time_get() = 4.002 act1 : i = 105, sched_rtime = 5, time_get() = 5 act1 : i = 106, sched_rtime = 6, time_get() = 6 act1 : i = 107, sched_rtime = 7, time_get() = 7 act2: i = 203, sched_rtime = 7, time_get() = 7 act1 : i = 108, sched_rtime = 8, time_get() = 8.001 act1 : i = 109, sched_rtime = 9, time_get() = 9 act1 : i = 110, sched_rtime = 10, time_get() = 10 act2: i = 204, sched_rtime = 10, time_get() = 10 act1 : i = 111, sched_rtime = 11, time_get() = 11.001 act1 : i = 112, sched_rtime = 12, time_get() = 12.002 ^C
Here is some output generated by the program. Notice that there are two independent “threads” or sequences of calls to activity, even though there in only one function. The events are separated by 1 second, with one set of events starting at 1 and the other starting at 1.1.
require "debug" require "sched" def activity(i): display "activity", i, sched_rtime, time_get() sched_cause(1, nil, 'activity', i + 1) sched_init() sched_cause(1, nil, 'activity', 101) sched_cause(1.1, nil, 'activity', 201) sched_run()
activity: i = 101, sched_rtime = 1, time_get() = 1.002 activity: i = 201, sched_rtime = 1.1, time_get() = 1.101 activity: i = 102, sched_rtime = 2, time_get() = 2.001 activity: i = 202, sched_rtime = 2.1, time_get() = 2.102 activity: i = 103, sched_rtime = 3, time_get() = 3 activity: i = 203, sched_rtime = 3.1, time_get() = 3.1 activity: i = 104, sched_rtime = 4, time_get() = 4 activity: i = 204, sched_rtime = 4.1, time_get() = 4.1 activity: i = 105, sched_rtime = 5, time_get() = 5.001 activity: i = 205, sched_rtime = 5.1, time_get() = 5.101 ^C
A good solution uses a counter rather than a flag, and every
sequence of calls (every “thread”) has an identifier that must
match the counter.
Here is some output generated by the program. Notice that the first “threads” is effectivly killed at time 4.5 when activity_id is incremented, but the thread does not “know” this until it wakes up at time 5, “sees” activity_id has changed, and terminates (returns without performing any action or scheduling any future calls).
require "debug" require "sched" activity_id = 0 // used to kill "threads" def activity(id, i): if id != activity_id: display "activity terminates", id, sched_rtime, time_get() return display "activity", id, i, sched_rtime, time_get() sched_cause(1, nil, 'activity', id, i + 1) def activity_start(i): // kill any old running activity activity_id = activity_id + 1 activity(activity_id, i) // start a new activity "thread" sched_init() sched_cause(1, nil, 'activity_start', 101) sched_cause(4.5, nil, 'activity_start', 201) sched_run()
activity: id = 1, i = 101, sched_rtime = 1, time_get() = 1.001
activity: id = 1, i = 102, sched_rtime = 2, time_get() = 2.002 activity: id = 1, i = 103, sched_rtime = 3, time_get() = 3.001 activity: id = 1, i = 104, sched_rtime = 4, time_get() = 4.001 activity: id = 2, i = 201, sched_rtime = 4.5, time_get() = 4.5 activity terminates: id = 1, sched_rtime = 5, time_get() = 5.001 activity: id = 2, i = 202, sched_rtime = 5.5, time_get() = 5.501 activity: id = 2, i = 203, sched_rtime = 6.5, time_get() = 6.501 activity: id = 2, i = 204, sched_rtime = 7.5, time_get() = 7.501 ^C
Here is some output generated by the program.
require "debug" require "sched" def activity(i): display "activity", i, sched_rtime, time_get() sched_cause(0.1, nil, 'subactivity', 0) sched_cause(1, nil, 'activity', i + 1) def subactivity(j): display " sub", j, sched_rtime, time_get() if j < 2: sched_cause(0.1, nil, 'subactivity', j + 1)
activity: i = 101, sched_rtime = 1, time_get() = 1.001 sub: j = 0, sched_rtime = 1.1, time_get() = 1.102 sub: j = 1, sched_rtime = 1.2, time_get() = 1.202 sub: j = 2, sched_rtime = 1.3, time_get() = 1.3 activity: i = 102, sched_rtime = 2, time_get() = 2.002 sub: j = 0, sched_rtime = 2.1, time_get() = 2.101 sub: j = 1, sched_rtime = 2.2, time_get() = 2.2 sub: j = 2, sched_rtime = 2.3, time_get() = 2.3 activity: i = 103, sched_rtime = 3, time_get() = 3.001 sub: j = 0, sched_rtime = 3.1, time_get() = 3.101 sub: j = 1, sched_rtime = 3.2, time_get() = 3.201 sub: j = 2, sched_rtime = 3.3, time_get() = 3.302 ^C
require "debug" require "sched" def activity(i): display "activity", i, sched_rtime, time_get(), sched_vtime sched_cause(1, nil, 'activity', i + 1) sched_init() sched_select(vtsched) sched_set_bps(2) // 2 beats per second // sched_trace = t sched_cause(1, nil, 'activity', 101) sched_run()
Here’s the output. Notice sched_vtime (which is the
logical time of vtsched) is not even close to real time
because this is virtual time (or beats, if you prefer).
activity: i = 101, sched_rtime = 0.5, time_get() = 0.5, sched_vtime = 1 activity: i = 102, sched_rtime = 1, time_get() = 1.002, sched_vtime = 2 activity: i = 103, sched_rtime = 1.5, time_get() = 1.502, sched_vtime = 3 activity: i = 104, sched_rtime = 2, time_get() = 2.001, sched_vtime = 4 activity: i = 105, sched_rtime = 2.5, time_get() = 2.502, sched_vtime = 5 ^C
require "debug" require "sched" def activity(i): display "activity", i, sched_rtime, time_get(), sched_vtime sched_cause(1, nil, 'activity', i + 1) sched_init() sched_select(vtsched) sched_set_bps(2) // 2 beats per second // equivalent to: sched_set_period(0.5) sched_cause(1, nil, 'activity', 101) sched_cause(4, nil, 'sched_set_bps', 0.5) sched_run()
Here's the output. Notice the real times (time_get()
)
are every 0.5 s until beat 4, then every 2 s.
activity: i = 101, sched_rtime = 1, time_get() = 0.5 activity: i = 101, sched_rtime = 0.5, time_get() = 0.5, sched_vtime = 1 activity: i = 102, sched_rtime = 1, time_get() = 1.002, sched_vtime = 2 activity: i = 103, sched_rtime = 1.5, time_get() = 1.501, sched_vtime = 3 activity: i = 104, sched_rtime = 2, time_get() = 2, sched_vtime = 4 activity: i = 105, sched_rtime = 4, time_get() = 4.002, sched_vtime = 5 activity: i = 106, sched_rtime = 6, time_get() = 6.002, sched_vtime = 6 ^C
Since we are going to talk about scheduling in wxserpent64, we
need a basic understanding of graphical user interfaces in
wxserpent64. For much more detail, see wxserpent
= serpent + wxWidgets and Serpent
by Example. If you are already familiar with wxserpent’s GUI
support, you can skip to Scheduling in
wxserpent64
wxserpent automatically creates default_window
(and
a text output window if there is any text output).
Graphical objects -- buttons, windows, menus, canvases to paint
on -- are all represented by Serpent objects.
require "debug" require "wxserpent" button = Button(0, "Hello", 5, 5, 75, 20) button.method = 'button_pressed' def button_pressed(obj, event, x, y): print "Hello World
Notes:
require "debug" require "wxserpent" file_menu = default_window.get_menu("File") file_menu.item("Hello", "Print Hello World", false, nil, 'file_menu_handler') def file_menu_handler(obj, event, x, y): display "file_menu_handler", obj, event, x, y print "Hello World"
Notes:
require "debug" require "wxserpent" require "slider" slider = Labeled_slider(0, "Volume", 5, 5, 250, 20, 70, 0, 1, 0.5, 'linear') slider.method = 'slider_handler' def slider_handler(obj, x): display "slider_handler", obj, x print "Slider changed to:", x
Notes:
require "debug" require "sched" def activity(i): display "activity", i, sched_rtime, time_get() sched_cause(1, nil, 'activity', i + 1) sched_init() sched_cause(1, nil, 'activity', 101)
Notes:
require "debug" require "wxserpent" require "sched" require "midi-io" require "prefs" require "mididevice" def activity(p): display "activity", p, sched_rtime, time_get() midi_out.note_on(0, 60 + p % 12, 0) time_sleep(0.1) p = p + 1 midi_out.note_on(0, 60 + p % 12, 100) sched_cause(0.2, nil, 'activity', p) sched_init() prefs = Prefs("./prefs.txt") midi_devices = Midi_devices(prefs, open_later = true) success = midi_devices.open_midi(latency = 10, device = 'midi_out_device') if not success print "PLEASE SELECT A VALID OUTPUT DEVICE AND RESTART THIS PROGRAM" else sched_cause(1, nil, 'activity', 0)
Notes:
require "debug" require "wxserpent" require "sched" require "midi-io" require "prefs" require "mididevice" require "slider" player_id = 0 // use "stopping a thread" pattern def activity(id, p): display "activity", p, sched_rtime, time_get() midi_out.note_on(0, 60 + p % 12, 0) if id != player_id: // stop AFTER note-off return p = p + 1 var vel = int(velocity.value()) midi_out.note_on(0, 60 + p % 12, vel) sched_cause(1, nil, 'activity', id, p) def stop(rest ignore) player_id = player_id + 1 Button(0, "Stop", 5, 5, 50, 20).method = 'stop' def set_period(obj, x): display "set_period", obj, x sched_select(vtsched) sched_set_period(x) period = Labeled_slider(0, "Period", 5, 30, 250, 20, 70, 0.05, 5, 0.2, 'exponential') period.method = 'set_period' velocity = Labeled_slider(0, "Velocity", 5, 55, 250, 20, 70, 1, 127, 100, 'linear') sched_init() // creates vtsched and rtsched prefs = Prefs("./prefs.txt") midi_devices = Midi_devices(prefs, open_later = true) success = midi_devices.open_midi(latency = 10, device = 'midi_out_device') if not success print "PLEASE SELECT A VALID OUTPUT DEVICE AND RESTART THIS PROGRAM" else sched_select(vtsched) sched_set_period(0.2) sched_cause(real_delay(5), nil, 'activity', player_id, 0)
Notes:
The previous examples do some behind-the-scenes work in schedulers and in midi-io to pass logical times as timestamps to PortMidi, the MIDI interface library used by Serpent. This section aims to reveal how this works.
PortMidi supports forward synchronous scheduling. In brief,
everything is timestamped. The goal is to compute everything
slightly ahead of real time, pass timestamped messages to the
device driver, and let the device driver send the messages
according to the timestamp to obtain very precise timing.
Forward synchronous scheduling is supported by the midi-io
library which simplifies the process. You should use it to get
better timing, especially when using wxserpent (because graphical
redisplays can be slow, and the timer callback will not be issued
while redrawing the display), but only if you can tolerate some
latency.
require "debug" require "wxserpent" require "sched" require "midi-io" require "prefs" require "mididevice" require "slider" player_id = 0 // use "stopping a thread" pattern def activity(id, p): time_sleep(random() * jitter.value()) midi_out.note_on(0, 60 + p % 24, 0) if id != player_id: // stop AFTER note-off return p = p + 1 midi_out.note_on(0, 60 + p % 24, 100) sched_cause(0.08, nil, 'activity', id, p) def stop(rest ignore) player_id = player_id + 1 Button(0, "Stop", 5, 5, 50, 20).method = 'stop' jitter = Labeled_slider(0, "Jitter", 5, 55, 250, 20, 70, 0, 0.05, 0, 'linear') sched_init() // creates vtsched and rtsched prefs = Prefs("./prefs.txt") midi_devices = Midi_devices(prefs, open_later = true) success = midi_devices.open_midi(latency = 0, device = 'midi_out_device') if not success print "PLEASE SELECT A VALID OUTPUT DEVICE AND RESTART THIS PROGRAM" else sched_cause(1, nil, 'activity', player_id, 0)
Notice that in the call to open_midi the latency is explicitly set to zero, which means timestamps are not used by PortMidi, and messages are delivered immediately. This still works pretty well, but if you crank the jitter slider up to 25 ms, you should notice timing irregularities.
(Code available here.) To get forward synchronous behavior in the previous program, just change latency = 0 to latency = 25.
Now, if you crank the jitter slider up to 25ms, the program will
run with same timing irregularities as before, but the MIDI
output timing will be close to perfect!
Let's look under the hood and see how this happens.
start()
method passes latency
(25) to PortMidi:
def start(keyword latency = 0, keyword midi_out_dev) if port != nil return var dev = midi_out_dev if not dev: dev = midi_out_default() if dev != -1: port = midi_create() if midi_open_output(port, midi_dev, 100, latency) != 0: return false else: print "Opened MIDI out device successfully:", dev print_midi_device_info(dev) else: print "No MIDI output device opened"; if midi_out_dev == -1: print " by request or preference setting." else: print ". There appears to be no output device." return false return t |
def note_on(chan, key, loud): var ticks = rtsched.get_tick() if midi_trace display "midi note_on", ticks, chan, key, loud chan = chan & 15 midi_write(port, ticks, chr(midi_status_noteon + chan) + (key << 8) + (loud << 16)) if loud > 0: midi_note_on_count = midi_note_on_count + 1 else: midi_note_off_count = midi_note_off_count + 1 |
# return an idealized integer count of milliseconds corresponding # to the timestamp used by PortMidi. Note that the timestamp used # by PortMidi is equal to int(time_get() * 1000) def get_tick() // note: if you are not running the scheduler, time_offset is nil // and this will raise an (intentional) error. It means you are // trying to schedule MIDI data before starting the scheduler. // Also, if you are not running a scheduled event, you must call // start_use() before get_tick(). Otherwise, the time field // will be the ideal time of the last event that ran, so it // may be a time in the distant past. if sched_nesting == 0 print "ERROR: Scheduler::get_tick() called, but not in an event" int((time + time_offset) * 1000) |
refresh()
operation that redraws a Canvas
object at 30 frames per second.
require "debug" require "wxserpent" require "sched" def activity(i): display "activity", i, sched_rtime, time_get() // 30 frames per second sched_rcause(1/30, nil, 'activity', i + 1) if random() < 0.03 // 3% of the time move rectangle animate.change_square() animate.refresh(t) // t means true, redraw everything def timer_callback() rtsched.poll(time_get()) class Animate (Canvas) var ex, ey // location of ellipse var sx, sy // location of square var sr, sg, sb // square color (red, green, blue) def init(parent, x, y, w, h) super.init(parent, x, y, w, h) // calls Canvas's init() ex = 20 // initialize this subclass of Canvas ey = 20 sx = 100 sy = 100 sr = 200 sg = 200 sb = 200 def paint(x) // x == true means redraw everything // this code always redraws everything //display "Animate::paint", x set_brush_color("GRAY") draw_ellipse(ex, ey, 50, 30) ex = ex + 3 if ex > get_width() - 50 ex = 20 set_brush_rgb(sr, sg, sb) draw_rectangle(sx, sy, 40, 40) def change_square() sx = random() * animate.get_width() sy = random() * animate.get_height() sr = int(random() * 256) sg = int(random() * 256) sb = int(random() * 256) def main() animate = Animate(WXS_DEFAULT_WINDOW, 0, 0, 300, 300) sched_init() rtsched.cause(1, nil, 'activity', 101) rtsched.time_offset = time_get() wxs_timer_start(2, 'timer_callback') main()
Notes:
In addition to scheduling function and method calls, as
illustrated here, Serpent also offers non-preemptive threads which
can suspend and resume. Threads can be scheduled using real and
virtual time schedulers, thus offering precisely timed execution.
See this page
on using threads.