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
@@ -0,0 +1,14 @@
|
|
1
|
+
(wav = WavOut.new(:filename => "ex04.wav")) >> blackhole
|
2
|
+
s = SawOsc.new(:freq => 440, :gain => 0.5)
|
3
|
+
adsr = ADSR.new(:attack_time => 50.ms,
|
4
|
+
:attack_gain => 1.0,
|
5
|
+
:decay_time => 50.ms,
|
6
|
+
:sustain_gain => 0.5,
|
7
|
+
:release_time => 1.second)
|
8
|
+
s >> adsr >> wav
|
9
|
+
|
10
|
+
play 1.second
|
11
|
+
adsr.on
|
12
|
+
play 2.seconds
|
13
|
+
adsr.off
|
14
|
+
play 2.seconds
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# run ex01.rb first
|
2
|
+
|
3
|
+
spoken = WavIn.new(:filename => "ex01.wav")
|
4
|
+
wav = WavOut.new(:filename => "ex06.wav")
|
5
|
+
spoken.out(0) >> wav >> blackhole
|
6
|
+
|
7
|
+
(sin = SinOsc.new(:freq => 3, :gain => 0.1)) >> blackhole
|
8
|
+
spoken.rate = L{ 1.0 + sin.last }
|
9
|
+
|
10
|
+
play spoken.duration
|
@@ -0,0 +1,35 @@
|
|
1
|
+
@bpm = 130.0
|
2
|
+
@one_beat = (1.0 / @bpm).minutes
|
3
|
+
|
4
|
+
(@wav = WavOut.new(:filename => "ex07.wav")) >> blackhole
|
5
|
+
|
6
|
+
def smash(len = @one_beat)
|
7
|
+
(n = Noise.new(:gain => 0.4)) >> (a = ADSR.new) >> @wav
|
8
|
+
a.release_time = len
|
9
|
+
a.on; play @one_beat / 1.5
|
10
|
+
a.off; play len
|
11
|
+
a << @wav
|
12
|
+
end
|
13
|
+
|
14
|
+
def beat
|
15
|
+
(thump = SawOsc.new(:freq => 220, :gain => 0.7)) >> (a = ADSR.new) >> @wav
|
16
|
+
a.on; play @one_beat / 2.0
|
17
|
+
a.off; play a.release_time
|
18
|
+
a << @wav
|
19
|
+
end
|
20
|
+
|
21
|
+
4.times do
|
22
|
+
spork("beat 1") { beat }; spork("smash") { smash }
|
23
|
+
play @one_beat
|
24
|
+
|
25
|
+
spork("beat 2") { beat }
|
26
|
+
play @one_beat
|
27
|
+
|
28
|
+
spork("beat 3") { beat }
|
29
|
+
play @one_beat
|
30
|
+
|
31
|
+
spork("beat 4") { beat }
|
32
|
+
play @one_beat
|
33
|
+
end
|
34
|
+
|
35
|
+
smash(5.seconds)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# An experiment with formants
|
2
|
+
# http://en.wikipedia.org/wiki/Formant
|
3
|
+
|
4
|
+
wav = WavOut.new(:filename => "ex08.wav")
|
5
|
+
ramps = (1..4).map { Ramp.new(:duration => 50.ms) }
|
6
|
+
oscillators = (1..4).map { SinOsc.new }
|
7
|
+
[oscillators >> wav, ramps] >> blackhole
|
8
|
+
|
9
|
+
(0..3).each { |i| oscillators[i].freq = L{ ramps[i].last } }
|
10
|
+
|
11
|
+
#vowel_ah = [1000, 1400]
|
12
|
+
#vowel_eh = [500, 2300]
|
13
|
+
#vowel_oh = [500, 1000]
|
14
|
+
vowel_ee = [[320, 1.0], [2500, 1.0], [3200, 1.0], [4600, 0.6]]
|
15
|
+
vowel_oo = [[320, 1.0], [800, 0.3], [2500, 0.1], [3300, 0.1]]
|
16
|
+
|
17
|
+
5.times do
|
18
|
+
[vowel_ee, vowel_oo].each do |vowel|
|
19
|
+
puts "doing #{vowel.inspect}"
|
20
|
+
(0..3).each do |i|
|
21
|
+
ramps[i].reset
|
22
|
+
ramps[i].from = ramps[i].to
|
23
|
+
ramps[i].to = vowel[i].first
|
24
|
+
oscillators[i].gain = vowel[i].last / 4.0
|
25
|
+
end
|
26
|
+
play 1.second
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# run ex01.rb first (or, preferably, use way better input than ex01.wav)
|
2
|
+
|
3
|
+
=begin
|
4
|
+
https://lists.cs.princeton.edu/pipermail/chuck-users/2008-May/002983.html
|
5
|
+
|
6
|
+
> One way of subjectively "widening" a stereo image is to do the following:
|
7
|
+
> feed the left channel back to the right with a short delay, inverted;
|
8
|
+
> feed the right channel back to the left with a short delay, inverted;
|
9
|
+
=end
|
10
|
+
|
11
|
+
mix = 0.5
|
12
|
+
|
13
|
+
wavin = WavIn.new :filename => "ex01.wav", :gain => (1.0 - mix)
|
14
|
+
wavout = WavOut.new :filename => "ex09.wav", :num_channels => 2
|
15
|
+
|
16
|
+
wavin.out(0) >> (delayed_left = Delay.new :time => 10.ms, :gain => mix)
|
17
|
+
wavin.out(1) >> (delayed_right = Delay.new :time => 10.ms, :gain => mix)
|
18
|
+
inverted_left = Step.new :value => L{ -delayed_left.next(now) }
|
19
|
+
inverted_right = Step.new :value => L{ -delayed_right.next(now) }
|
20
|
+
|
21
|
+
wavout >> blackhole
|
22
|
+
[wavin.out(0), inverted_right] >> wavout.in(0)
|
23
|
+
[wavin.out(1), inverted_left ] >> wavout.in(1)
|
24
|
+
|
25
|
+
play wavin.duration
|
26
|
+
puts "processed #{wavin.duration/SAMPLE_RATE} seconds"
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# adds 60 Hz hum to a stereo wav file
|
2
|
+
|
3
|
+
wavin = WavIn.new :filename => "ex11.wav", :gain => 0.5
|
4
|
+
wavout = WavOut.new :filename => "ex12.wav", :num_channels => 2
|
5
|
+
wavin >> wavout
|
6
|
+
SinOsc.new(:freq => 60, :gain => 0.5) >> wavout
|
7
|
+
wavout >> blackhole
|
8
|
+
|
9
|
+
play 3.seconds
|
data/lib/ruck.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
|
2
|
+
module Ruck
|
3
|
+
require "logger"
|
4
|
+
LOG = Logger.new(STDOUT)
|
5
|
+
LOG.level = Logger::WARN
|
6
|
+
end
|
7
|
+
|
8
|
+
require File.join(File.dirname(__FILE__), "ruck", "shreduling")
|
9
|
+
require File.join(File.dirname(__FILE__), "ruck", "misc", "metaid")
|
10
|
+
require File.join(File.dirname(__FILE__), "ruck", "misc", "linkage")
|
11
|
+
require File.join(File.dirname(__FILE__), "ruck", "ugen", "general")
|
12
|
+
require File.join(File.dirname(__FILE__), "ruck", "ugen", "wav")
|
13
|
+
require File.join(File.dirname(__FILE__), "ruck", "ugen", "oscillators")
|
data/lib/ruck/bench.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
SAMPLE_RATE = 44100
|
3
|
+
|
4
|
+
if ARGV.include? "bench.rb"
|
5
|
+
# benchmark UGens with shreduling
|
6
|
+
|
7
|
+
TIME = 1.seconds
|
8
|
+
count = 0
|
9
|
+
puts "Simulating #{TIME / 1.second} seconds"
|
10
|
+
loop do
|
11
|
+
Step.new >> blackhole
|
12
|
+
count += 1
|
13
|
+
|
14
|
+
start = Time.now
|
15
|
+
play TIME
|
16
|
+
time = Time.now - start
|
17
|
+
puts "#{count}: #{time}"
|
18
|
+
|
19
|
+
break if time > (TIME / 1.second)
|
20
|
+
end
|
21
|
+
else
|
22
|
+
# benchmark UGens without shreduling
|
23
|
+
|
24
|
+
require "ruck"
|
25
|
+
TIME = 1.seconds
|
26
|
+
dac = Ruck::InChannel.new
|
27
|
+
count = 0
|
28
|
+
@now = 0
|
29
|
+
puts "Simulating #{TIME / 1.second} seconds"
|
30
|
+
loop do
|
31
|
+
Ruck::Generators::Step.new >> dac
|
32
|
+
count += 1
|
33
|
+
|
34
|
+
start = Time.now
|
35
|
+
TIME.to_i.times do
|
36
|
+
dac.next(@now)
|
37
|
+
@now += 1
|
38
|
+
end
|
39
|
+
time = Time.now - start
|
40
|
+
puts "#{count}: #{time}"
|
41
|
+
|
42
|
+
break if time > (TIME / 1.second)
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
class Object
|
3
|
+
def linkable_attr(attr_sym)
|
4
|
+
attr_reader attr_sym
|
5
|
+
define_method("#{attr_sym}=") do |val|
|
6
|
+
instance_variable_set("@#{attr_sym}", val)
|
7
|
+
if val.respond_to? :call
|
8
|
+
meta_def attr_sym do
|
9
|
+
val.call
|
10
|
+
end
|
11
|
+
else
|
12
|
+
meta_def attr_sym do
|
13
|
+
val
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def L(&block)
|
20
|
+
block
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# stolen from:
|
2
|
+
# http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
|
3
|
+
|
4
|
+
class Object
|
5
|
+
# The hidden singleton lurks behind everyone
|
6
|
+
def metaclass; class << self; self; end; end
|
7
|
+
def meta_eval &blk; metaclass.instance_eval &blk; end
|
8
|
+
|
9
|
+
# Adds methods to a metaclass
|
10
|
+
def meta_def name, &blk
|
11
|
+
meta_eval { define_method name, &blk }
|
12
|
+
end
|
13
|
+
|
14
|
+
# Defines an instance method within a class
|
15
|
+
def class_def name, &blk
|
16
|
+
class_eval { define_method name, &blk }
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
module RuckTime
|
3
|
+
def sample
|
4
|
+
self
|
5
|
+
end
|
6
|
+
alias_method :samples, :sample
|
7
|
+
|
8
|
+
def ms
|
9
|
+
self.to_f * SAMPLE_RATE / 1000.0
|
10
|
+
end
|
11
|
+
|
12
|
+
def second
|
13
|
+
self.to_f * SAMPLE_RATE
|
14
|
+
end
|
15
|
+
alias_method :seconds, :second
|
16
|
+
|
17
|
+
def minute
|
18
|
+
self.to_f * SAMPLE_RATE * 60.0
|
19
|
+
end
|
20
|
+
alias_method :minutes, :minute
|
21
|
+
end
|
22
|
+
|
23
|
+
class Fixnum
|
24
|
+
include RuckTime
|
25
|
+
end
|
26
|
+
|
27
|
+
class Float
|
28
|
+
include RuckTime
|
29
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
|
2
|
+
module Riff
|
3
|
+
|
4
|
+
class RiffReaderChunk
|
5
|
+
attr_reader :data_start
|
6
|
+
attr_accessor :data_skip
|
7
|
+
|
8
|
+
def initialize(fn, start)
|
9
|
+
@fn, @start = fn, start
|
10
|
+
@size_start = @start + 4
|
11
|
+
@data_start = @start + 8
|
12
|
+
@data_skip = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def type
|
16
|
+
return @type if @type
|
17
|
+
@fn.seek @start
|
18
|
+
@type = @fn.read(4)
|
19
|
+
end
|
20
|
+
|
21
|
+
def size
|
22
|
+
return @size - @data_skip if @size
|
23
|
+
@fn.seek @size_start
|
24
|
+
@size = @fn.read(4).unpack("L")[0]
|
25
|
+
@size - @data_skip
|
26
|
+
end
|
27
|
+
|
28
|
+
# pass a Range of bytes, or start and length
|
29
|
+
def [](*args)
|
30
|
+
first, last = case args.length
|
31
|
+
when 1; [args.first.begin, args.first.end]
|
32
|
+
when 2; [args[0], args[0] + args[1]]
|
33
|
+
end
|
34
|
+
@fn.seek @data_start + @data_skip + first
|
35
|
+
@fn.read(last - first + 1)
|
36
|
+
end
|
37
|
+
|
38
|
+
def chunks
|
39
|
+
offset = @data_start + @data_skip
|
40
|
+
chunks = []
|
41
|
+
while offset + @data_skip - @data_start < size
|
42
|
+
chunks << chunk = RiffReaderChunk.new(@fn, offset)
|
43
|
+
offset += @data_start + chunk.size
|
44
|
+
end
|
45
|
+
chunks
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
"<RiffHeader type:#{type} size:#{size}>"
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
class RiffReader
|
55
|
+
def initialize(filename)
|
56
|
+
@fn = File.open(filename, "rb")
|
57
|
+
end
|
58
|
+
|
59
|
+
def chunks
|
60
|
+
offset = 0
|
61
|
+
chunks = []
|
62
|
+
until @fn.eof?
|
63
|
+
chunks << chunk = RiffReaderChunk.new(@fn, offset)
|
64
|
+
offset += 8 + chunk.size
|
65
|
+
@fn.seek offset + 8
|
66
|
+
end
|
67
|
+
chunks
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# stolen from
|
2
|
+
# http://www.mokehehe.com/assari/index.php?WAV%A5%D5%A5%A1%A5%A4%A5%EB%C6%E2%A4%CE%A5%C1%A5%E3%A5%F3%A5%AF%A4%F2%CE%F3%B5%F3%A4%B9%A4%EB
|
3
|
+
|
4
|
+
if ARGV.length < 1
|
5
|
+
print "usage: [input wav filename]\n"
|
6
|
+
exit
|
7
|
+
end
|
8
|
+
|
9
|
+
inwavfn = ARGV[0]
|
10
|
+
|
11
|
+
File::open(inwavfn, "rb") {|f|
|
12
|
+
riff = f.read(4)
|
13
|
+
if riff != 'RIFF'
|
14
|
+
STDERR.print "not RIFF\n"
|
15
|
+
exit
|
16
|
+
end
|
17
|
+
totalsizestr = f.read(4)
|
18
|
+
totalsize = totalsizestr.unpack("L")[0]
|
19
|
+
|
20
|
+
wave = f.read(4)
|
21
|
+
if wave != 'WAVE'
|
22
|
+
STDERR.print "not WAVE\n"
|
23
|
+
exit
|
24
|
+
end
|
25
|
+
|
26
|
+
while !f.eof
|
27
|
+
chk = f.read(4)
|
28
|
+
chksizestr = f.read(4)
|
29
|
+
chksize = chksizestr.unpack("L")[0]
|
30
|
+
|
31
|
+
print chk, ",#{chksize}\n"
|
32
|
+
|
33
|
+
f.seek(chksize, IO::SEEK_CUR)
|
34
|
+
end
|
35
|
+
}
|
@@ -0,0 +1,147 @@
|
|
1
|
+
|
2
|
+
module Ruck
|
3
|
+
|
4
|
+
class Shred
|
5
|
+
attr_reader :now
|
6
|
+
attr_accessor :finished
|
7
|
+
|
8
|
+
def initialize(shreduler, now, name, &block)
|
9
|
+
@shreduler = shreduler
|
10
|
+
@now = now
|
11
|
+
@name = name
|
12
|
+
@block = block
|
13
|
+
@finished = false
|
14
|
+
end
|
15
|
+
|
16
|
+
def go(resume)
|
17
|
+
@resume = resume
|
18
|
+
begin
|
19
|
+
@block.call self
|
20
|
+
rescue => e
|
21
|
+
LOG.error "#{self} exited uncleanly:\n#{e}\n#{e.backtrace}"
|
22
|
+
end
|
23
|
+
@finished = true
|
24
|
+
end
|
25
|
+
|
26
|
+
def yield(samples)
|
27
|
+
LOG.debug "shred #{self} yielding #{samples}"
|
28
|
+
samples = samples
|
29
|
+
@now += samples
|
30
|
+
callcc do |cont|
|
31
|
+
@block = cont # save where we are
|
32
|
+
@resume.call # jump back to shreduler
|
33
|
+
end
|
34
|
+
samples
|
35
|
+
end
|
36
|
+
|
37
|
+
def finish
|
38
|
+
@resume.call # last save point
|
39
|
+
end
|
40
|
+
|
41
|
+
# shreds sort in order of position in time
|
42
|
+
def <=>(shred)
|
43
|
+
@now <=> shred.now
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
"<Shred: #{@name}>"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Shreduler
|
52
|
+
attr_reader :running
|
53
|
+
attr_reader :current_shred
|
54
|
+
attr_reader :shreds
|
55
|
+
attr_reader :now
|
56
|
+
|
57
|
+
def initialize
|
58
|
+
@shreds = []
|
59
|
+
@now = 0
|
60
|
+
@running = false
|
61
|
+
end
|
62
|
+
|
63
|
+
def spork(name = "", &shred)
|
64
|
+
LOG.debug "Adding shred \"#{name}\" at #{@now}"
|
65
|
+
@shreds << Shred.new(self, @now, name, &shred)
|
66
|
+
@shred
|
67
|
+
end
|
68
|
+
|
69
|
+
def remove_shred(shred)
|
70
|
+
LOG.debug "Removing shred \"#{name}\" at #{@now}"
|
71
|
+
@shreds.delete shred
|
72
|
+
end
|
73
|
+
|
74
|
+
def next_shred
|
75
|
+
@shreds.min # furthest behind (Shred#<=> uses Shred's current time)
|
76
|
+
end
|
77
|
+
|
78
|
+
# called when shreds allow time to pass
|
79
|
+
# a convnient method to override
|
80
|
+
def sim_to(new_now)
|
81
|
+
@now = new_now
|
82
|
+
end
|
83
|
+
|
84
|
+
# invokes the next shred, simulates to the new VM time, then returns
|
85
|
+
def run_one
|
86
|
+
@current_shred = next_shred
|
87
|
+
|
88
|
+
sim_to(@current_shred.now)
|
89
|
+
|
90
|
+
# execute shred, saving this as the resume point
|
91
|
+
LOG.debug "resuming shred #{@current_shred} at #{now}"
|
92
|
+
callcc { |cont| @current_shred.go(cont) }
|
93
|
+
LOG.debug "back to run loop"
|
94
|
+
|
95
|
+
if @current_shred.finished
|
96
|
+
LOG.debug "#{@current_shred} finished"
|
97
|
+
@shreds.delete(@current_shred)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# ruck main loop
|
102
|
+
# executes all shreds and synthesizes audio
|
103
|
+
# until all shreds exit
|
104
|
+
def run
|
105
|
+
LOG.debug "shreduler starting"
|
106
|
+
@running = true
|
107
|
+
|
108
|
+
while @shreds.length > 0
|
109
|
+
run_one
|
110
|
+
end
|
111
|
+
|
112
|
+
@running = false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class UGenShreduler < Shreduler
|
117
|
+
def run
|
118
|
+
require File.join(File.dirname(__FILE__), "misc", "pcm_time_helpers")
|
119
|
+
super
|
120
|
+
end
|
121
|
+
|
122
|
+
def sim_to(new_now)
|
123
|
+
while @now < new_now.to_i
|
124
|
+
BLACKHOLE.next @now
|
125
|
+
@now += 1
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class RealTimeShreduler < Shreduler
|
131
|
+
def run
|
132
|
+
@start_time = Time.now
|
133
|
+
super
|
134
|
+
end
|
135
|
+
|
136
|
+
def sim_to(new_now)
|
137
|
+
actual_now = Time.now
|
138
|
+
simulated_now = @start_time + (new_now.to_f / SAMPLE_RATE)
|
139
|
+
if simulated_now > actual_now
|
140
|
+
sleep(simulated_now - actual_now)
|
141
|
+
end
|
142
|
+
|
143
|
+
@now = new_now
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|