ruck 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ pkg
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,13 @@
1
+
2
+ clear
3
+
4
+ loop do
5
+ wait 2
6
+
7
+ glTranslate(0, 0, -5)
8
+ glutSolidCube(2)
9
+
10
+ wait 0.1
11
+
12
+ clear
13
+ end
@@ -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