ruck

ChucK is an audio programming language, the most productivity-enhancing DSL I've ever used, and I kind of use it for everything. It has two great things going for it:

  • Connections between audio processing units can be reconfigured programmatically on the fly with a syntax so simple that usually, if you can think it, you can code it in a few minutes.
  • You can reconfigure those audio processing units with any time resolution you desire: there are no fixed control rates.

Both are practical features, but the second is what captures my imagination. To the point that I haven't been able to think about anything but "strongly-timed" scheduling for a long time. The idea is simple and powerful, but underutilized as far as I've seen.

Time should be a first-class citizen in a programming language. ruck does this for Ruby.

require "ruck"
@shreduler = Ruck::Shreduler.new
@shreduler.make_convenient
@shreduler.run

ruck works by introducing shredulers. Shredulers shredule sporked shreds on a virtual timeline. Unlike threads, shreds stay in sync without explicit synchronization.

spork do
  puts "ping"
  Shred.yield(2)
end

spork do
  Shred.yield(1)
  puts "pong"
  Shred.yield(1)
end

Shreds are cooperatively multi-shredded to maintain the illusion that code execution is instantaneous. All code shreduled for a particular time is allowed to finish before later code is run.

spork do
  sleep 15
end

spork do
  Shred.yield(1)
  puts "real time: +15 seconds"
  puts "virtual time: +1 time unit"
end

How virtual time maps to real time is up to you. The default Ruck::Shreduler executes everything as quickly as it can, but is written to be easily sub-classed for more complex behavior. The most obvious sub-class makes one time unit equivalent to one second.

# imprecise, but effective real-time shreduling
class RealTimeShreduler < Ruck::Shreduler
  def fast_forward(dt)
    super
    sleep(dt)
  end
end

RealTimeShreduler.new.make_convenient

spork do
  t = 0
  loop do
    puts "#{t}..."
    Shred.yield(1)
    t += 1
  end
end

$shreduler.run

Or you can do like ChucK and map virtual time units to samples in an audio stream.

class AudioShreduler < Ruck::Shreduler
  def fast_forward(dt)
    super

    @audio_now ||= 0
    @synthesis.process(self.now.to_i - @audio_now)
    @audio_now = now.to_i
  end
end

spork do
  osc = SinOscillator.new
  output.connect(osc)

  loop do
    osc.freq = rand * 400 + 200
    Shred.yield(44100)
  end
end

Or you can map to MIDI ticks, frames of video, or bytes in a file. But real-time shredulers are the most generally useful. It's easiest to think about time as time. The problem with typical render loops is that they force you to think in frames and buffers instead of time. ruck can help you there.

class RenderLoopShreduler < Ruck::Shreduler
  def catch_up
    @start_time ||= Time.now
    real_now = Time.now - @start_time
    run_until(real_now)
  end
end

class GameWindow < Gosu::Window
  def initialize
    super(640, 480, false, 16)
    RenderLoopShreduler.new.make_convenient

    @images = Gosu::Image::load_tiles(self, "animation.png", 25, 25, false)
    @frame = 0

    spork_loop(:draw) do
      @images[@frame].draw(320, 240)
    end

    spork_loop(0.1) do
      @frame = (@frame + 1) % @images.size
    end
  end

  def draw
    $shreduler.raise_all(:draw)
    $shreduler.catch_up
  end
end

I'm scratching the surface of ruck here, not to mention the potential of strong timing. There are plenty of examples in the ruck-realtime, ruck-midi, ruck-ugen, and ruck-glapp projects, not to mention ruck itself.

If you're interested, I recommend starting with ruck's README.

Did I level up with this post?


Comments

Click here to view the comments on this post, or just send me an e-mail.