ruck 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|