Permalink
Browse files

Extracted Gamelan::Queue as a facade to decouple the Scheduler from t…

…he queue implementation. The Scheduler's rate is specified in Hz. Refactored Scheduler so that the phase is manipulated only within Scheduler#advance. Added some RDoc.
  • Loading branch information...
1 parent 0d911c8 commit 8ea0a6fd80c5fde831edcf675fc43090179d594c Jeremy Voorhis committed Nov 29, 2008
Showing with 90 additions and 39 deletions.
  1. 0 README
  2. +27 −0 README.rdoc
  3. +2 −2 benchmark/benchmark.rb
  4. +1 −1 lib/gamelan.rb
  5. +24 −0 lib/gamelan/queue.rb
  6. +22 −30 lib/gamelan/scheduler.rb
  7. +14 −6 lib/gamelan/task.rb
View
0 README
No changes.
View
@@ -0,0 +1,27 @@
+= Gamelan
+
+Gamelan is a good-enough soft real-time event scheduler especially for music
+applications. It exposes a simple API for executing Ruby code at a required
+time. Uses include sending MIDI or OSC messages to external applications or
+hardware.
+
+Gamelan also makes life easier by supporting logical time. Logical time is
+reflected in the scheduler's phase. The unit in logical time is the beat, and
+the Scheduler's phase will increment by 1.0 with every beat.
+
+Logical time varies with real time according to the tempo, which is specified
+in bpm. For example, the Scheduler's phase will increment by 2.0 for every
+second that elapses when using the default tempo of 120bpm. Applications are
+free to alter the tempo at any time, including from within tasks.
+
+= Notes
+
+The author admits that Ruby is not at all friendly to realtime applications.
+No guarantees are made about the scheduler's performance. It will not drift
+(it will always stay in sync with the system clock), but jitter is inevitable.
+This is minimized by using a hybrid spinlock implementation to wait between
+dispatches, and by using a reasonably efficient priority queue to store Tasks.
+
+The design is an elaboration of Topher Cyll's Timer implementation from his
+book, <em>Practical Ruby Projects</em>, and the Priority Queue implementation
+comes from Brian Amberg.
View
@@ -22,7 +22,7 @@
stddev = Math.sqrt(var)
puts "sample size\t\t%i" % samples.size
-puts "average error\t\t%.5fms" % (error.inject(0) { |a,b| a + b } / error.size)
-puts "peak error\t\t%.5fms" % error.max
+puts "average jitter\t\t%.5fms" % (error.inject(0) { |a,b| a + b } / error.size)
+puts "peak jitter\t\t%.5fms" % error.max
puts "standard deviation\t%.5fms" % stddev
#puts error.map { |sec| sprintf("%2.3fms", sec) }
View
@@ -9,7 +9,7 @@
require 'gamelan/scheduler'
require 'gamelan/task'
-class Object
+class Object # :nodoc:
unless methods.include?('instance_exec')
# Like instace_eval but allows parameters to be passed.
View
@@ -0,0 +1,24 @@
+require 'priority_queue/c_priority_queue'
+require 'priority_queue'
+
+module Gamelan
+ class Queue
+ def initialize(sched)
+ @scheduler = sched
+ @queue = ::PriorityQueue.new
+ end
+
+ def push(task)
+ @queue.push(task, task.delay)
+ end
+ alias << push
+
+ def pop
+ @queue.delete_min[0]
+ end
+
+ def ready?
+ @queue.min && @queue.min[1] < @scheduler.phase
+ end
+ end
+end
View
@@ -1,19 +1,22 @@
-require 'thread'
-require 'priority_queue/c_priority_queue'
-require 'priority_queue'
+require 'gamelan/queue'
module Gamelan
+ # The scheduler is responsible for scheduling tasks and running them as accurately as possible.
class Scheduler
- attr_reader :phase, :queue, :rate, :thread, :time
+ attr_reader :phase, :rate, :time
+ # Construct a new scheduler. +Scheduler#run+ must be called explicitly once a Scheduler is created. Accepts two options, +:tempo+ and +:rate+.
+ # [+:tempo+] The tempo's scheduler, in bpm. For example, at +:tempo => 120+, the scheduler's logical +phase+ will advance by 2.0 every 60 seconds.
+ # [+:rate+] Frequency in Hz at which the scheduler will attempt to run ready tasks. For example, The scheduler will poll for tasks 100 times in one
+ # second when +:rate+ is 100.
def initialize(options = {})
self.tempo = options.fetch(:tempo, 120)
- @rate = options.fetch(:rate, 0.001)
+ @rate = 1.0 / options.fetch(:rate, 1000)
@sleep_for = rate / 10.0
- @queue = PriorityQueue.new
- @lock = Mutex.new
+ @queue = Gamelan::Queue.new(self)
end
+ # Initialize the scheduler's clock, and begin executing tasks.
def run
return if @running
@running = true
@@ -24,50 +27,39 @@ def run
end
end
+ # Halt the scheduler. Note that the scheduler is not yet resumable.
def stop
@running = false
-
@thread.kill
- @thread = nil
- @origin = @time = @phase = nil
end
- # Add a new job to be performed at +time+.
+ # Schedule a task to be performed at +delay+ beats.
def at(delay, *params, &task)
- @queue.push(Task.new(self, delay.to_f, *params, &task),
- delay.to_f)
- end
-
- def freeze(&block)
- @lock.synchronize(&block)
+ @queue << Task.new(self, delay.to_f, *params, &task)
end
+ # Current tempo, in bpm.
def tempo
@tempo * 60.0
end
+ # Set the tempo in bpm.
def tempo=(bpm)
@tempo = bpm / 60.0
end
private
- # Advance the internal clock time and spin until it is reached.
+ # Advances the internal clock time and spins until it is reached.
def advance
- @lock.synchronize do
- @time += @rate
- loop do
- break if Time.now.to_f >= @time
- sleep(@sleep_for) # Don't saturate the CPU
- end
- end
+ @time += @rate
+ @phase += (@time - @origin) * @tempo
+ @origin = @time
+ sleep(@sleep_for) until Time.now.to_f >= @time
end
+ # Run all ready tasks.
def dispatch
- @phase += (@time - @origin) * @tempo
- @origin = @time
- while @queue.min && @queue.min[1] < @phase
- @queue.delete_min[0].run
- end
+ @queue.pop.run while @queue.ready?
end
end
end
View
@@ -1,18 +1,26 @@
require 'forwardable'
module Gamelan
-
+
+ # Tasks are run by the Scheduler. A task is a combination of a block to be
+ # run, a delay, in beats, that specifies when to run the Task, and an
+ # optional list of args. A reference to the Scheduler is also stored, so it
+ # can be manipulatd by tasks.
class Task
extend Forwardable
- def_delegators :@scheduler, :at, :freeze, :phase, :rate, :thread, :time
- attr_reader :delay, :params, :scheduler
+ def_delegators :@scheduler, :at, :phase, :rate, :time
+ attr_reader :delay, :args, :scheduler
- def initialize(sched, delay, *params, &proc)
- @scheduler, @delay, @proc, @params = sched, delay, proc, params
+ # Construct a Task with a Scheduler reference, a delay in beats, an
+ # optional list of args, and a block.
+ def initialize(sched, delay, *args, &block)
+ @scheduler, @delay, @proc, @args = sched, delay, block, args
end
+ # Task#run is yielded within the scope of the Task when the Scheduler is
+ # ready. Any optional +args+ are yielded to the block.
def run
- instance_exec(*params, &@proc)
+ instance_exec(*@args, &@proc)
end
end
end

0 comments on commit 8ea0a6f

Please sign in to comment.