ckv blog

Subscribe to the Atom feed.

mesmerizing

Posted February 16, 2010

Happy Tuesday.

clock = Clock(120)
scale = { 0, 3, 7, 10 }
base = 40

function beep()
  local o = PulseOsc(440)
  c(o, speaker)
  while true do
    o.freq = mtof(base + scale[random(1, #scale)])
    o.width = rand()
    yield(0.5, clock)
  end
end

fork(beep)
fork(beep)
fork(beep)
fork(beep)
fork(beep)
fork(beep)

function beep2()
  local o = PulseOsc(440)
  local g = Gain()
  c(o, g)
  g.gain = 0
  local m = SinOsc(0.03)
  m.phase = 0.7
  c(m, blackhole)
  while true do
    fork(function()
      c(g, dac)
      o.freq = mtof(base + 24 + scale[#scale])
      o.width = rand()
      yield(0.25, clock)
      d(g, dac)
    end)
    yield(0.5, clock)
    g.gain = m.last * 0.5 + 0.5
  end
end

fork(beep2)

function tick(gg)
  local n = Noise(0.1)
  local g = Gain(gg)
  c(n, g, dac)
  yield(10 * ms)
  d(g, dac)
end

fork(function()
  while true do
    fork(tick, 1)
    yield(1, clock)
    fork(tick, .5)
    yield(1, clock)
    fork(tick, .5)
    yield(1, clock)
    fork(tick, .5)
    yield(1, clock)
  end
end)

while yield(16, clock) do
  base = random(base - 2, base + 3)
  clock.bpm = clock.bpm * (1 + rand() * 0.02)
end

Linux Support!

Posted February 15, 2010

I finally freakin' did it. There exist commands by the names gcc and g++ on both OS X and Linux, but they are by no means the same.

Anyway, if you change the PLATFORM=OSX of the Makefile to say PLATFORM=LINUX, then it ought to build and work, assuming you have all the dependencies. Tested on Ubuntu and Gentoo with the latest from the repository. The tutorial has been updated.

I will probably begin to supply occasional binaries now. ckv's feeling like a real project these days.


Clock, and huge documentation update

Posted February 15, 2010

A beat isn't really a unit of time. Although beat = second / 2 is good enough for lots of electronic music, for any music where the tempo will change over time, yield(beat) is insufficient because threads will get out of sync if beat ever changes unless great care is taken.

This was the motivation for introducing Clock objects to ckv. Clocks allow you to use musically meaningful units when yielding time, and won't let threads get out of sync when you change a beat's duration. Here's an example:

clock = Clock(300)

fork(function()
  while sync(1, clock) do
    local n = Noise()
    c(n, speaker)
    yield(100 * ms)
    disconnect(n, speaker)
  end
end)

clock_mod = SinOsc(0.1)
c(clock_mod, blackhole)

while true do
  clock.bpm = clock_mod.last * 90 + 100
  print("bpm:", clock.bpm)
  yield(100 * ms)
end

now, yield, and sync have been updated to work with clocks by taking one as the last argument.

… which you would know if you had checked out the just-updated Language Reference page (formerly "ugen Reference"), which now documents ugens, events, clocks, forking, the global namespace, and more. Check it out, it's so long.


sync()

Posted February 8, 2010

I've never been much of a live-coder, but I've been doing a bit of it in ChucK recently to avoid more staggeringly inefficient ckv syntax like connect(…) (now aliased to c(…)). I've recorded two of my attempts on Vimeo – here and here – but haven't perfected my recording methods or musical sense, so it's probably hard work to get through them both. ☺

Anyway, a trick I learned from one of kijjaz' videos was to prepend scripts with second - (now % second) => now so that whenever you add them to the VM, they'll wait for the next whole second before they begin. This quantization gives you a little extra flexibility in when you press the hotkey for adding a shred.

So now that's built into ckv as sync(second), or sync(minute / 140) or whatever your sync period is.


ugen ticking optimization

Posted January 17, 2010

To reduce overhead and boilerplate in the ugens' tick functions, I've just committed code that makes some important changes:

  1. Each ugen's tick function is called exactly once per sample
  2. Rather than return the next sample, a ugen's tick should set self.last
  3. Speaking of which, ugen.last_value is now ugen.last
  4. Calling UGen.initialize_io() is no longer required and it has been removed

So while I previously recommended writing a ugen like this:

Noise = {
  new = function(class, gain)
    return UGen.initialize_io({
      gain = gain or 1.0,
      last_value = 0.0,
      tick = function(self)
        if not(now() == self.last_tick) then
          self.last_value = (math.random() * 2.0 - 1.0) * self.gain;
          self.last_tick = now();
        end
        return self.last_value
      end
    })
  end
}

You can now slim that down to something like this:

Noise = {
  new = function(class, gain)
    return {
      gain = gain or 1.0,
      tick = function(self)
        self.last = (math.random() * 2.0 - 1.0) * self.gain;
      end
    }
  end
}

The changes to how ugens are ticked are not trivial, so please let me know about any bugs I have introduced. :)

The way I ensure that ugens are ticked only once is by sorting the ugen graph by longest path length from speaker & blackhole, and caching the resulting queue until the next time a connection is made or broken. All in all I see a ~20% speed improvement (using ex01.ckv as a benchmark), which I expect to be greater on more complicated graphs, and less on graphs that change frequently.


distortion ugen

Posted January 10, 2010

The source is in a crazy state with a bunch of stuff not checked in, otherwise this ugen would probably be in by now. It soft-clips, then caps the first derivative for some interesting distortion. It's a port of some ChucK I wrote a while back; it's great to be able to write UGens like this. :)

Distort = {
  new = function(class, pregain)
    return UGen.initialize_io({
      last_value = 0.0,
      pregain = pregain or 100.0,
      max_slew = 0.5,
      tick = function(self)
        if not(now() == self.last_tick) then
          local i = UGen.sum_inputs(self) * self.pregain;
          local out = 0.0;

          -- make roundy
          if i >= 1 then
            out = 2.0 / 3.0
          elseif i <= -1 then
            out = -2.0 / 3.0
          else
            out = i - (i * i * i) / 3.0
          end

          -- max slew
          if math.abs(out - self.last_value * 2.0 / 3.0) > self.max_slew then
            if out < self.last_value * 2.0 / 3.0 then
              out = self.last_value * 2.0 / 3.0 - self.max_slew
            else
              out = self.last_value * 2.0 / 3.0 + self.max_slew
            end
          end

          self.last_value = out * 3.0 / 2.0;
          self.last_tick = now();
        end
        return self.last_value
      end
    })
  end
}

ugen reference

Posted January 6, 2010

In-between airport concourse sprints today, I wrote up a reference page for unit generators.

I also added an example to the tutorial for SndIn.

I'd say more but… exhausted. Good night. ;p


ckv tutorial

Posted January 5, 2010

I spent a few hours throwing together some introductory material on ckv. The installation section assumes that you're extremely comfortable on the command-line (something I soon hope to remedy), but the rest should be pretty straight-forward without being simplistic. I want anybody to be able to read the tutorial and get going.

I'm going to use the tutorial as a walk-through for every single feature of ckv (a choose-your-own-adventure examples directory) in addition to traditional language and unit generator reference pages.


Delay ugen

Posted January 4, 2010

I created a Delay unit generator in Lua to see how much we can expect where large arrays of samples are concerned. At 44100 sample rate, this doesn't start dropping samples for me until the buffer's at least a couple hundred milliseconds long. A second-long delay line clicks every second, but it's definitely not as bad as I was expecting.

Delay = {
  new = function(class, delay)
    return UGen.initialize_io({
      delay = delay or second,
      last_value = 0.0,
      last_values = {},
      tick = function(self)
        if not(now() == self.last_tick) then
          local vals = self.last_values;
          local offset = math.ceil(self.delay); -- no sub-samples for you!

          vals[#vals + 1] = UGen.sum_inputs(self);

          self.last_value = vals[#vals - offset] or 0.0;

          if #vals > offset * 2 then
            for i = 1, offset, 1 do
              vals[i] = vals[#vals - offset + i]
            end

            for i = offset + 1, #vals, 1 do
              vals[i] = nil
            end
          end

          self.last_tick = now();
        end
        return self.last_value
      end
    })
  end
}

Home