ruck 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/README +85 -0
- data/Rakefile +25 -0
- data/VERSION +1 -0
- data/bin/ruck_glapp +77 -0
- data/bin/ruck_midi +143 -0
- data/bin/ruck_ugen +53 -0
- data/examples/glapp/ex01.rb +13 -0
- data/examples/midi/ex01.rb +24 -0
- data/examples/ugen/ex01.rb +24 -0
- data/examples/ugen/ex02.rb +2 -0
- data/examples/ugen/ex03.rb +8 -0
- data/examples/ugen/ex04.rb +14 -0
- data/examples/ugen/ex05.rb +9 -0
- data/examples/ugen/ex06.rb +10 -0
- data/examples/ugen/ex07.rb +35 -0
- data/examples/ugen/ex08.rb +28 -0
- data/examples/ugen/ex09.rb +26 -0
- data/examples/ugen/ex10.rb +15 -0
- data/examples/ugen/ex11.rb +10 -0
- data/examples/ugen/ex12.rb +9 -0
- data/lib/ruck.rb +13 -0
- data/lib/ruck/bench.rb +44 -0
- data/lib/ruck/misc/linkage.rb +22 -0
- data/lib/ruck/misc/metaid.rb +18 -0
- data/lib/ruck/misc/pcm_time_helpers.rb +29 -0
- data/lib/ruck/misc/riff.rb +71 -0
- data/lib/ruck/misc/wavparse.rb +35 -0
- data/lib/ruck/shreduling.rb +147 -0
- data/lib/ruck/ugen/general.rb +408 -0
- data/lib/ruck/ugen/oscillators.rb +106 -0
- data/lib/ruck/ugen/wav.rb +185 -0
- data/ruck.gemspec +94 -0
- metadata +102 -0
data/.gitignore
ADDED
data/README
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
A port of ChucK's strong timing to Ruby!
|
2
|
+
|
3
|
+
Use Ruck's "shreds" to coordinate interleaved execution
|
4
|
+
of threads (continuations) with precise virtual timing.
|
5
|
+
Actual run-time is abstracted and shreds declare how much
|
6
|
+
virtual time they took to execute. Scheduling of shreds
|
7
|
+
is done by a ...
|
8
|
+
|
9
|
+
SHREDULER
|
10
|
+
|
11
|
+
UGenShreduler:
|
12
|
+
|
13
|
+
This shreduler calculates an audio unit generator graph's
|
14
|
+
output in virtual time. The graph can be modified by
|
15
|
+
shreds all the while.
|
16
|
+
|
17
|
+
The graph is written in Ruby for flexibility, so it's
|
18
|
+
too slow (on my computer) for real time, so there is no
|
19
|
+
real-time playback. You can use WavOut, though.
|
20
|
+
(See ex01.rb)
|
21
|
+
|
22
|
+
Check out the library of built-in unit generators in ugen/
|
23
|
+
and make your own.
|
24
|
+
|
25
|
+
Unit Generator Usage
|
26
|
+
====================
|
27
|
+
|
28
|
+
Play a sine wave (ex02.rb):
|
29
|
+
|
30
|
+
s = SinOsc.new(:freq => 440)
|
31
|
+
w = WavOut.new(:filename => "ex02.wav")
|
32
|
+
s >> w >> blackhole
|
33
|
+
play 3.seconds
|
34
|
+
|
35
|
+
Attach lambdas to unit generator attributes (ex03.rb):
|
36
|
+
|
37
|
+
wav = WavOut.new(:filename => "ex03.wav")
|
38
|
+
sin2 = SinOsc.new(:freq => 3)
|
39
|
+
sin = SinOsc.new(:freq => L{ sin2.last * 220 + 660 },
|
40
|
+
:gain => L{ 0.5 + sin2.last * 0.5 })
|
41
|
+
[sin >> wav, sin2] >> blackhole
|
42
|
+
play 3.seconds
|
43
|
+
|
44
|
+
|
45
|
+
RealTimeShreduler
|
46
|
+
|
47
|
+
This shreduler attempts to keep virtual time in line with
|
48
|
+
real time.
|
49
|
+
(See ex10.rb)
|
50
|
+
|
51
|
+
MIDIShreduler
|
52
|
+
|
53
|
+
This shreduler uses MIDIator and midilib to support live
|
54
|
+
MIDI playback and saving MIDI to disk. An example runner
|
55
|
+
is provided in midi_runner.rb which you invoke like this:
|
56
|
+
|
57
|
+
$ ruby midilib_runner.rb MIDI_FILENAME NUM_TRACKS LIVE SCRIPT_FILENAME [...]
|
58
|
+
|
59
|
+
where LIVE is "no" to only save the MIDI output, or "yes"
|
60
|
+
to also play in real-time.
|
61
|
+
|
62
|
+
USAGE
|
63
|
+
=====
|
64
|
+
|
65
|
+
ugen_runner.rb is an (example) program that lets you run scripts
|
66
|
+
as you do with ChucK, with a shreduler that pulls samples from
|
67
|
+
the unit generator graph (through the blackhole):
|
68
|
+
|
69
|
+
$ ruby ugen_runner.rb examples/ex01.rb
|
70
|
+
|
71
|
+
It uses a UGenShreduler with a hard-coded sample rate. You can use it
|
72
|
+
as an example for how to embed your own shreduler. It boils down
|
73
|
+
to this:
|
74
|
+
|
75
|
+
require "ruck"
|
76
|
+
|
77
|
+
@shreduler = Ruck::Shreduler.new # replace with your subclass
|
78
|
+
|
79
|
+
# repeat this part as necessary to seed @shreduler with shreds
|
80
|
+
@shreduler.spork("main") do
|
81
|
+
...
|
82
|
+
end
|
83
|
+
|
84
|
+
# this returns when no shreds remain
|
85
|
+
@shreduler.run
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
begin
|
3
|
+
require "jeweler"
|
4
|
+
Jeweler::Tasks.new do |gemspec|
|
5
|
+
gemspec.name = "ruck"
|
6
|
+
gemspec.email = "tom@alltom.com"
|
7
|
+
gemspec.homepage = "http://github.com/alltom/ruck"
|
8
|
+
gemspec.authors = ["Tom Lieber"]
|
9
|
+
gemspec.summary = "strong timing for Ruby: cooperative threads on a virtual clock"
|
10
|
+
gemspec.description = <<-EOF
|
11
|
+
Ruck uses continuations and a simple scheduler to ensure "shreds"
|
12
|
+
(threads in Ruck) are woken at precisely the right time according
|
13
|
+
to its virtual clock. Schedulers can map virtual time to samples
|
14
|
+
in a WAV file, real time, time in a MIDI file, or anything else
|
15
|
+
by overriding "sim_to" in the Shreduler class.
|
16
|
+
|
17
|
+
A small library of useful unit generators and plenty of examples
|
18
|
+
are provided. See the README or the web page for details.
|
19
|
+
EOF
|
20
|
+
gemspec.has_rdoc = false
|
21
|
+
end
|
22
|
+
Jeweler::GemcutterTasks.new
|
23
|
+
rescue LoadError
|
24
|
+
puts "Jewler not available. Install it with: sudo gem install jeweler"
|
25
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/ruck_glapp
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
|
4
|
+
# requires glapp gem
|
5
|
+
# sudo gem install alltom-glapp
|
6
|
+
# see http://alltom.com/pages/glapp for more details
|
7
|
+
|
8
|
+
require "ruck"
|
9
|
+
|
10
|
+
require "rubygems"
|
11
|
+
require "glapp"
|
12
|
+
|
13
|
+
class GLAppShreduler < Ruck::Shreduler
|
14
|
+
def initialize
|
15
|
+
@shreds_waiting_for_next_frame = []
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def actual_now
|
20
|
+
Time.now - @start_time
|
21
|
+
end
|
22
|
+
|
23
|
+
def enqueue_for_next_frame(shred)
|
24
|
+
@shreds_waiting_for_next_frame << shred
|
25
|
+
@shreds.delete(shred)
|
26
|
+
end
|
27
|
+
|
28
|
+
def next_frame_has_arrived
|
29
|
+
@shreds += @shreds_waiting_for_next_frame
|
30
|
+
|
31
|
+
@shreds_waiting_for_next_frame.each do |shred|
|
32
|
+
shred.now = now
|
33
|
+
end
|
34
|
+
|
35
|
+
@shreds_waiting_for_next_frame = []
|
36
|
+
end
|
37
|
+
|
38
|
+
def catch_up
|
39
|
+
@start_time ||= Time.now
|
40
|
+
|
41
|
+
while shreds.length > 0 && next_shred.now < actual_now
|
42
|
+
run_one
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module ShredLocal
|
48
|
+
def wait_for_frame
|
49
|
+
SHREDULER.enqueue_for_next_frame(SHREDULER.current_shred)
|
50
|
+
SHREDULER.current_shred.yield(0)
|
51
|
+
end
|
52
|
+
|
53
|
+
def wait(seconds)
|
54
|
+
SHREDULER.current_shred.yield(seconds)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class MySketch
|
59
|
+
def setup
|
60
|
+
ARGV.each do |filename|
|
61
|
+
SHREDULER.spork(filename) do
|
62
|
+
require filename
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def draw
|
68
|
+
SHREDULER.catch_up
|
69
|
+
SHREDULER.next_frame_has_arrived
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
include GLApp
|
74
|
+
include ShredLocal
|
75
|
+
SHREDULER = GLAppShreduler.new
|
76
|
+
|
77
|
+
MySketch.new.show 800, 600, "My Sketch"
|
data/bin/ruck_midi
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
|
4
|
+
require "ruck"
|
5
|
+
|
6
|
+
require "rubygems"
|
7
|
+
require "midilib"
|
8
|
+
require "midiator"
|
9
|
+
|
10
|
+
# configuration
|
11
|
+
if ARGV.length < 4
|
12
|
+
puts "ruby midilib_runner.rb MIDI_FILENAME NUM_TRACKS LIVE SCRIPT_FILENAME [...]"
|
13
|
+
exit 1
|
14
|
+
end
|
15
|
+
|
16
|
+
MIDI_FILENAME = ARGV[0]
|
17
|
+
NUM_TRACKS = ARGV[1].to_i
|
18
|
+
ALSO_LIVE = ["yes", "true", "1", "yeah"].include? ARGV[2].downcase
|
19
|
+
FILENAMES = ARGV[3..-1]
|
20
|
+
|
21
|
+
class MIDIShreduler < Ruck::Shreduler
|
22
|
+
def run
|
23
|
+
@start_time = Time.now
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def sim_to(new_now)
|
28
|
+
d = new_now - @now
|
29
|
+
TRACK_DELTAS.each_with_index do |delta, i|
|
30
|
+
TRACK_DELTAS[i] = delta + d
|
31
|
+
end
|
32
|
+
|
33
|
+
# sync with wall clock
|
34
|
+
if ALSO_LIVE
|
35
|
+
actual_now = Time.now
|
36
|
+
simulated_now = @start_time + (new_now.to_f / SEQUENCE.ppqn / SEQUENCE.bpm * 60.0)
|
37
|
+
if simulated_now > actual_now
|
38
|
+
sleep(simulated_now - actual_now)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
@now = new_now
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# state
|
47
|
+
SHREDULER = MIDIShreduler.new
|
48
|
+
SEQUENCE = MIDI::Sequence.new
|
49
|
+
TRACKS = (1..NUM_TRACKS).map { MIDI::Track.new(SEQUENCE) }
|
50
|
+
TRACK_DELTAS = TRACKS.map { 0 }
|
51
|
+
if ALSO_LIVE
|
52
|
+
MIDI_PLAYER = MIDIator::Interface.new
|
53
|
+
end
|
54
|
+
|
55
|
+
# midi initialization stuff
|
56
|
+
TRACKS.each do |track|
|
57
|
+
SEQUENCE.tracks << track
|
58
|
+
#track.events << MIDI::Tempo.new(MIDI::Tempo.bpm_to_mpq(120))
|
59
|
+
end
|
60
|
+
if ALSO_LIVE
|
61
|
+
MIDI_PLAYER.use :dls_synth
|
62
|
+
MIDI_PLAYER.instruct_user!
|
63
|
+
end
|
64
|
+
|
65
|
+
# set up some useful time helpers
|
66
|
+
module MIDITime
|
67
|
+
def pulse
|
68
|
+
self
|
69
|
+
end
|
70
|
+
alias_method :pulses, :pulse
|
71
|
+
|
72
|
+
def quarter_note
|
73
|
+
self * SEQUENCE.ppqn
|
74
|
+
end
|
75
|
+
alias_method :quarter_notes, :quarter_note
|
76
|
+
alias_method :beat, :quarter_note
|
77
|
+
alias_method :beats, :quarter_note
|
78
|
+
end
|
79
|
+
|
80
|
+
class Fixnum
|
81
|
+
include MIDITime
|
82
|
+
end
|
83
|
+
|
84
|
+
class Float
|
85
|
+
include MIDITime
|
86
|
+
end
|
87
|
+
|
88
|
+
# stuff accessible in a shred
|
89
|
+
module ShredLocal
|
90
|
+
|
91
|
+
def now
|
92
|
+
SHREDULER.now
|
93
|
+
end
|
94
|
+
|
95
|
+
def spork(name = "unnamed", &shred)
|
96
|
+
SHREDULER.spork(name, &shred)
|
97
|
+
end
|
98
|
+
|
99
|
+
def wait(pulses)
|
100
|
+
SHREDULER.current_shred.yield(pulses)
|
101
|
+
end
|
102
|
+
|
103
|
+
def finish
|
104
|
+
shred = SHREDULER.current_shred
|
105
|
+
SHREDULER.remove_shred shred
|
106
|
+
shred.finish
|
107
|
+
end
|
108
|
+
|
109
|
+
def note_on(note, velocity = 127, channel = 0, track = 0)
|
110
|
+
TRACKS[track].events << MIDI::NoteOnEvent.new(channel, note, velocity, TRACK_DELTAS[track].to_i)
|
111
|
+
TRACK_DELTAS[track] = 0
|
112
|
+
if ALSO_LIVE
|
113
|
+
MIDI_PLAYER.driver.note_on(note, channel, velocity)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def note_off(note, channel = 0, track = 0)
|
118
|
+
TRACKS[track].events << MIDI::NoteOffEvent.new(channel, note, 0, TRACK_DELTAS[track].to_i)
|
119
|
+
TRACK_DELTAS[track] = 0
|
120
|
+
if ALSO_LIVE
|
121
|
+
MIDI_PLAYER.driver.note_on(note, channel, 0)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
FILENAMES.each do |filename|
|
128
|
+
unless File.readable?(filename)
|
129
|
+
LOG.fatal "Cannot read file #{filename}"
|
130
|
+
exit
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
FILENAMES.each do |filename|
|
135
|
+
SHREDULER.spork(filename) do
|
136
|
+
include ShredLocal
|
137
|
+
require filename
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
SHREDULER.run
|
142
|
+
|
143
|
+
File.open(MIDI_FILENAME, "wb") { |file| SEQUENCE.write(file) }
|
data/bin/ruck_ugen
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
3
|
+
|
4
|
+
require "ruck"
|
5
|
+
|
6
|
+
# stuff accessible in a shred
|
7
|
+
module ShredLocal
|
8
|
+
|
9
|
+
def blackhole
|
10
|
+
BLACKHOLE
|
11
|
+
end
|
12
|
+
|
13
|
+
def now
|
14
|
+
SHREDULER.now
|
15
|
+
end
|
16
|
+
|
17
|
+
def spork(name = "unnamed", &shred)
|
18
|
+
SHREDULER.spork(name, &shred)
|
19
|
+
end
|
20
|
+
|
21
|
+
def play(samples)
|
22
|
+
SHREDULER.current_shred.yield(samples)
|
23
|
+
end
|
24
|
+
|
25
|
+
def finish
|
26
|
+
shred = SHREDULER.current_shred
|
27
|
+
SHREDULER.remove_shred shred
|
28
|
+
shred.finish
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
SAMPLE_RATE = 22050
|
35
|
+
SHREDULER = Ruck::UGenShreduler.new
|
36
|
+
BLACKHOLE = Ruck::InChannel.new
|
37
|
+
|
38
|
+
filenames = ARGV
|
39
|
+
filenames.each do |filename|
|
40
|
+
unless File.readable?(filename)
|
41
|
+
LOG.fatal "Cannot read file #{filename}"
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
filenames.each do |filename|
|
47
|
+
SHREDULER.spork(filename) do
|
48
|
+
include ShredLocal
|
49
|
+
include Ruck::Generators
|
50
|
+
require filename
|
51
|
+
end
|
52
|
+
end
|
53
|
+
SHREDULER.run
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
def maybe
|
3
|
+
rand >= 0.5
|
4
|
+
end
|
5
|
+
|
6
|
+
TRACKS[0].events << MIDI::Controller.new(10, 32, 1) # channel, controller, value
|
7
|
+
TRACKS[0].events << MIDI::ProgramChange.new(10, 26) # channel, program
|
8
|
+
MIDI_PLAYER.control_change 32, 10, 1 # number, channel, value
|
9
|
+
MIDI_PLAYER.program_change 10, 26 # channel, program
|
10
|
+
|
11
|
+
def play(note, dur = 1.quarter_note)
|
12
|
+
return if maybe
|
13
|
+
note_on note, 100, 10
|
14
|
+
wait dur
|
15
|
+
note_off note, 10
|
16
|
+
end
|
17
|
+
|
18
|
+
10.times do
|
19
|
+
spork { play(rand(30) + 40, rand * 3.quarter_notes + 3.quarter_notes) }
|
20
|
+
wait 0.5.quarter_note
|
21
|
+
end
|
22
|
+
|
23
|
+
wait 2.quarter_notes; note_off 0 # end padding
|
24
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
def beep(wav, chan)
|
2
|
+
(s = SawOsc.new(:freq => 440, :gain => 0.25)) >> wav.in(chan)
|
3
|
+
10.times do
|
4
|
+
play 0.1.seconds
|
5
|
+
s.freq *= 1.2
|
6
|
+
end
|
7
|
+
s << wav
|
8
|
+
end
|
9
|
+
|
10
|
+
wav = WavOut.new(:filename => "ex01.wav", :num_channels => 2)
|
11
|
+
SinOsc.new(:freq => 440, :gain => 0.25) >> wav
|
12
|
+
SinOsc.new(:freq => 880, :gain => 0.25) >> wav
|
13
|
+
|
14
|
+
wav >> blackhole
|
15
|
+
|
16
|
+
chan = 0
|
17
|
+
|
18
|
+
10.times do
|
19
|
+
play 0.7.seconds
|
20
|
+
chan = (chan + 1) % 2
|
21
|
+
spork("beep") { beep(wav, chan) }
|
22
|
+
end
|
23
|
+
|
24
|
+
play 2.seconds
|