roborabb 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY.md +5 -0
- data/README.md +61 -0
- data/Rakefile +12 -0
- data/lib/roborabb.rb +16 -0
- data/lib/roborabb/bar.rb +23 -0
- data/lib/roborabb/builder.rb +77 -0
- data/lib/roborabb/core_ext.rb +1 -0
- data/lib/roborabb/lilypond.rb +149 -0
- data/lib/roborabb/version.rb +3 -0
- data/roborabb.gemspec +28 -0
- data/spec/acceptance_spec.rb +22 -0
- data/spec/unit_spec.rb +259 -0
- metadata +82 -0
data/HISTORY.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
Roborabb
|
2
|
+
========
|
3
|
+
|
4
|
+
Generates drumming practice charts in [lilypond][lilypond] notation.
|
5
|
+
|
6
|
+
<img
|
7
|
+
src="https://img.skitch.com/20111210-n7ey6x4jrmiaq11tjj1j8qqd4u.jpg"
|
8
|
+
alt='example score' />
|
9
|
+
|
10
|
+
Example
|
11
|
+
-------
|
12
|
+
|
13
|
+
Install the gem:
|
14
|
+
|
15
|
+
gem install roborabb
|
16
|
+
|
17
|
+
Then use it:
|
18
|
+
|
19
|
+
require 'roborabb'
|
20
|
+
|
21
|
+
rock_1 = Roborabb.construct(
|
22
|
+
title: "Rock",
|
23
|
+
subdivisions: 8,
|
24
|
+
unit: 8,
|
25
|
+
time_signature: "4/4",
|
26
|
+
notes: {
|
27
|
+
hihat: L{|env| true },
|
28
|
+
kick: L{|env| (env.subdivision + 0) % 4 == 0 },
|
29
|
+
snare: L{|env| (env.subdivision + 2) % 4 == 0 },
|
30
|
+
}
|
31
|
+
)
|
32
|
+
|
33
|
+
puts Roborabb::Lilypond.new(rock_1, bars: 16).to_lilypond
|
34
|
+
|
35
|
+
The resulting file is immediately compilable with [lilypond][lilypond]:
|
36
|
+
|
37
|
+
ruby examples/rock.rb > rock.ly && lilypond rock.ly # Generates rock.pdf
|
38
|
+
|
39
|
+
See `examples` directory for more.
|
40
|
+
|
41
|
+
[lilypond]: http://lilypond.org/
|
42
|
+
|
43
|
+
Compatibility
|
44
|
+
-------------
|
45
|
+
|
46
|
+
Only tested on ruby 1.9.3. Require 1.9, since it uses new style hashes.
|
47
|
+
|
48
|
+
Developing
|
49
|
+
----------
|
50
|
+
|
51
|
+
git clone git://github.com/xaviershay/roborabb.git
|
52
|
+
bundle # Install development dependencies
|
53
|
+
bundle exec rake # Runs the specs
|
54
|
+
|
55
|
+
Any big new features require an acceptance test, bug fixes should only require
|
56
|
+
unit tests. Follow the conventions already present.
|
57
|
+
|
58
|
+
Status
|
59
|
+
------
|
60
|
+
|
61
|
+
New, but complete.
|
data/Rakefile
ADDED
data/lib/roborabb.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'roborabb/version'
|
2
|
+
|
3
|
+
require 'roborabb/core_ext'
|
4
|
+
require 'roborabb/builder'
|
5
|
+
require 'roborabb/lilypond'
|
6
|
+
|
7
|
+
module Roborabb
|
8
|
+
def construct(plan)
|
9
|
+
unless plan.has_key?(:notes)
|
10
|
+
raise(ArgumentError, "Plan does not contain :notes")
|
11
|
+
end
|
12
|
+
Builder.new(plan)
|
13
|
+
end
|
14
|
+
module_function :construct
|
15
|
+
end
|
16
|
+
|
data/lib/roborabb/bar.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Roborabb
|
2
|
+
class Bar
|
3
|
+
ATTRIBUTES = [
|
4
|
+
:beat_structure,
|
5
|
+
:notes,
|
6
|
+
:subdivisions,
|
7
|
+
:time_signature,
|
8
|
+
:title,
|
9
|
+
:unit,
|
10
|
+
]
|
11
|
+
attr_reader *ATTRIBUTES
|
12
|
+
|
13
|
+
def initialize(attributes)
|
14
|
+
ATTRIBUTES.each do |x|
|
15
|
+
send("#{x}=", attributes[x])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
attr_writer *ATTRIBUTES
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
require 'roborabb/bar'
|
4
|
+
|
5
|
+
module Roborabb
|
6
|
+
class Builder
|
7
|
+
attr_reader :plan
|
8
|
+
|
9
|
+
def initialize(plan_hash)
|
10
|
+
self.plan = OpenStruct.new(plan_hash)
|
11
|
+
self.bar_env = OpenStruct.new(index: 0)
|
12
|
+
self.enumerator = Enumerator.new do |yielder|
|
13
|
+
loop do
|
14
|
+
yielder.yield(generate_bar)
|
15
|
+
bar_env.index += 1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def next
|
21
|
+
enumerator.next
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def generate_bar
|
27
|
+
notes = subdivisions.inject(empty_notes) do |notes, subdivision|
|
28
|
+
env = build_env(subdivision)
|
29
|
+
|
30
|
+
plan.notes.map do |name, f|
|
31
|
+
notes[name] << resolve(f, env)
|
32
|
+
end
|
33
|
+
|
34
|
+
notes
|
35
|
+
end
|
36
|
+
|
37
|
+
Bar.new(
|
38
|
+
subdivisions: subdivisions.max + 1,
|
39
|
+
unit: resolve(plan.unit, bar_env),
|
40
|
+
time_signature: resolve(plan.time_signature, bar_env),
|
41
|
+
beat_structure: resolve(plan.beat_structure, bar_env),
|
42
|
+
title: resolve(plan.title, bar_env),
|
43
|
+
notes: notes
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def resolve(f, env)
|
48
|
+
if f.respond_to?(:call)
|
49
|
+
f.call(env)
|
50
|
+
else
|
51
|
+
f
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def subdivisions
|
56
|
+
(0...resolve(plan.subdivisions, bar_env))
|
57
|
+
end
|
58
|
+
|
59
|
+
def empty_notes
|
60
|
+
x = plan.notes.keys.map do |name|
|
61
|
+
[name, []]
|
62
|
+
end
|
63
|
+
Hash[x]
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_env(subdivision)
|
67
|
+
OpenStruct.new(
|
68
|
+
subdivision: subdivision,
|
69
|
+
bar: bar_env
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
attr_writer :plan
|
74
|
+
attr_accessor :enumerator
|
75
|
+
attr_accessor :bar_env
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
alias :L :lambda
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'roborabb/bar'
|
2
|
+
|
3
|
+
module Roborabb
|
4
|
+
class Lilypond
|
5
|
+
def initialize(generator, opts)
|
6
|
+
self.generator = generator
|
7
|
+
self.opts = opts
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_lilypond
|
11
|
+
score = opts[:bars].times.map do
|
12
|
+
bar = generator.next
|
13
|
+
|
14
|
+
format_bar(bar)
|
15
|
+
end
|
16
|
+
|
17
|
+
lilypond do
|
18
|
+
voice(:up) { format_bars(score, :upper) } +
|
19
|
+
voice(:down) { format_bars(score, :lower) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
attr_accessor :generator, :opts, :title
|
26
|
+
|
27
|
+
def format_bars(bars, voice)
|
28
|
+
last_plan = Bar.new({})
|
29
|
+
bars.map do |bar|
|
30
|
+
plan = bar[:bar]
|
31
|
+
|
32
|
+
preamble = ""
|
33
|
+
if last_plan.time_signature != plan.time_signature
|
34
|
+
preamble += %(\\time #{plan.time_signature}\n)
|
35
|
+
end
|
36
|
+
|
37
|
+
if last_plan.beat_structure != plan.beat_structure && plan.beat_structure
|
38
|
+
preamble += %(\\set Staff.beatStructure = #'(%s)\n) % [
|
39
|
+
plan.beat_structure.join(' ')
|
40
|
+
]
|
41
|
+
end
|
42
|
+
last_plan = plan
|
43
|
+
self.title = plan.title
|
44
|
+
|
45
|
+
preamble + bar[voice]
|
46
|
+
end.join(' | ') + ' \\bar "|."'
|
47
|
+
end
|
48
|
+
|
49
|
+
def lilypond
|
50
|
+
# Evaluating the content first is necessary to infer the title.
|
51
|
+
content = yield
|
52
|
+
|
53
|
+
<<-LP
|
54
|
+
\\version "2.14.2"
|
55
|
+
\\header {
|
56
|
+
title = "#{title}"
|
57
|
+
subtitle = " "
|
58
|
+
}
|
59
|
+
\\new DrumStaff <<
|
60
|
+
#{content}
|
61
|
+
>>
|
62
|
+
LP
|
63
|
+
end
|
64
|
+
|
65
|
+
def voice(direction)
|
66
|
+
result = <<-LP
|
67
|
+
\\new DrumVoice {
|
68
|
+
\\override Rest #'direction = ##{direction}
|
69
|
+
\\stem#{direction == :up ? "Up" : "Down"} \\drummode {
|
70
|
+
#{yield}
|
71
|
+
}
|
72
|
+
}
|
73
|
+
LP
|
74
|
+
end
|
75
|
+
|
76
|
+
def format_bar(bar)
|
77
|
+
{
|
78
|
+
bar: bar,
|
79
|
+
upper: format_notes(bar, expand(hashslice(bar.notes, :hihat))),
|
80
|
+
lower: format_notes(bar, expand(hashslice(bar.notes, :kick, :snare)))
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def format_notes(bar, notes)
|
85
|
+
notes.map do |note|
|
86
|
+
if note[0].length == 1
|
87
|
+
mappings[note[0][0]] + duration(bar, note[1]).to_s
|
88
|
+
elsif note[0].length > 1
|
89
|
+
"<%s>%s" % [
|
90
|
+
note[0].map {|x| mappings[x] }.join(' '),
|
91
|
+
duration(bar, note[1])
|
92
|
+
]
|
93
|
+
else
|
94
|
+
"r%s" % duration(bar, note[1])
|
95
|
+
end
|
96
|
+
end.join(" ")
|
97
|
+
end
|
98
|
+
|
99
|
+
def mappings
|
100
|
+
{
|
101
|
+
kick: 'bd',
|
102
|
+
snare: 'sn',
|
103
|
+
hihat: 'hh'
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
def duration(bar, x)
|
108
|
+
unit = bar.unit
|
109
|
+
[
|
110
|
+
unit,
|
111
|
+
unit / 2,
|
112
|
+
(unit / 2).to_s + ".",
|
113
|
+
unit / 4
|
114
|
+
].map(&:to_s)[x-1] || raise("Unsupported duration: #{x}")
|
115
|
+
end
|
116
|
+
|
117
|
+
def hashslice(hash, *keep_keys)
|
118
|
+
h = {}
|
119
|
+
keep_keys.each do |key|
|
120
|
+
h[key] = hash[key] if hash.has_key?(key)
|
121
|
+
end
|
122
|
+
h
|
123
|
+
end
|
124
|
+
|
125
|
+
def expand(notes)
|
126
|
+
accum = []
|
127
|
+
time = 0
|
128
|
+
out = notes.values.transpose.inject([]) do |out, on_notes|
|
129
|
+
on = [*on_notes].map.with_index do |x, i|
|
130
|
+
notes.keys[i] if x
|
131
|
+
end.compact
|
132
|
+
|
133
|
+
if !on.empty? || time >= 4
|
134
|
+
if time > 0
|
135
|
+
out << [accum, time]
|
136
|
+
end
|
137
|
+
accum = on
|
138
|
+
time = 0
|
139
|
+
end
|
140
|
+
time += 1
|
141
|
+
|
142
|
+
out
|
143
|
+
end
|
144
|
+
|
145
|
+
out << [accum, time] if time > 0
|
146
|
+
out
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
data/roborabb.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/roborabb/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Xavier Shay"]
|
6
|
+
gem.email = ["hello@xaviershay.com"]
|
7
|
+
gem.description = %q{Algorithmically generate practice drum scores}
|
8
|
+
gem.summary = %q{
|
9
|
+
Algorithmically generate practice drum scores. Customize algorithms with
|
10
|
+
ruby with an archaeopteryx-inspired style, output to lilypond format.
|
11
|
+
}
|
12
|
+
gem.homepage = "http://github.com/xaviershay/roborabb"
|
13
|
+
|
14
|
+
gem.executables = []
|
15
|
+
gem.files = Dir.glob("{spec,lib}/**/*.rb") + %w(
|
16
|
+
README.md
|
17
|
+
HISTORY.md
|
18
|
+
Rakefile
|
19
|
+
roborabb.gemspec
|
20
|
+
)
|
21
|
+
gem.test_files = Dir.glob("spec/**/*.rb")
|
22
|
+
gem.name = "roborabb"
|
23
|
+
gem.require_paths = ["lib"]
|
24
|
+
gem.version = Roborabb::VERSION
|
25
|
+
gem.has_rdoc = false
|
26
|
+
gem.add_development_dependency 'rspec', '~> 2.0'
|
27
|
+
gem.add_development_dependency 'rake'
|
28
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__))
|
2
|
+
require 'roborabb'
|
3
|
+
|
4
|
+
describe 'outputting to lilypond' do
|
5
|
+
it 'outputs a basic rock beat' do
|
6
|
+
rabb = Roborabb.construct(
|
7
|
+
subdivisions: 8,
|
8
|
+
unit: 8,
|
9
|
+
time_signature: "4/4",
|
10
|
+
notes: {
|
11
|
+
hihat: L{|env| true },
|
12
|
+
kick: L{|env| (env.subdivision + 0) % 4 == 0 },
|
13
|
+
snare: L{|env| (env.subdivision + 2) % 4 == 0 },
|
14
|
+
}
|
15
|
+
)
|
16
|
+
ly = Roborabb::Lilypond.new(rabb, bars: 2)
|
17
|
+
output = ly.to_lilypond.lines.map(&:chomp).join
|
18
|
+
output.should include('\\time 4/4')
|
19
|
+
output.should include('hh8 hh8 hh8 hh8 | hh8 hh8 hh8 hh8')
|
20
|
+
output.should include('bd4 sn4 | bd4 sn4')
|
21
|
+
end
|
22
|
+
end
|
data/spec/unit_spec.rb
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__))
|
2
|
+
require 'roborabb'
|
3
|
+
|
4
|
+
describe Roborabb do
|
5
|
+
def notes(rabb)
|
6
|
+
rabb.next.notes
|
7
|
+
end
|
8
|
+
|
9
|
+
def default_attributes
|
10
|
+
{
|
11
|
+
subdivisions: 2,
|
12
|
+
unit: 8,
|
13
|
+
time_signature: '1/4',
|
14
|
+
notes: {}
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def construct(attributes)
|
19
|
+
Roborabb.construct(default_attributes.merge(attributes))
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#construct' do
|
23
|
+
it 'raises Argument error when no :notes given' do
|
24
|
+
lambda {
|
25
|
+
Roborabb.construct(default_attributes.delete_if {|k, _| k == :notes })
|
26
|
+
}.should raise_error(ArgumentError)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#next' do
|
31
|
+
it 'allows a value for notes' do
|
32
|
+
rabb = construct(
|
33
|
+
subdivisions: 2,
|
34
|
+
notes: { a: 'A' }
|
35
|
+
)
|
36
|
+
|
37
|
+
notes(rabb).should == { a: ['A', 'A'] }
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'includes subdivision in env yielded to notes' do
|
41
|
+
rabb = construct(
|
42
|
+
subdivisions: 3,
|
43
|
+
notes: {
|
44
|
+
a: :subdivision.to_proc
|
45
|
+
}
|
46
|
+
)
|
47
|
+
|
48
|
+
notes(rabb).should == { a: [0, 1, 2] }
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'includes bar number in env yielded to config' do
|
52
|
+
rabb = construct(
|
53
|
+
subdivisions: L{|e| e.index + 1 },
|
54
|
+
notes: { a: 1 }
|
55
|
+
)
|
56
|
+
|
57
|
+
3.times.map { notes(rabb) }.should == [
|
58
|
+
{ a: [1] },
|
59
|
+
{ a: [1, 1] },
|
60
|
+
{ a: [1, 1, 1] }
|
61
|
+
]
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'includes bar number in env yielded to notes' do
|
65
|
+
rabb = construct(
|
66
|
+
subdivisions: 2,
|
67
|
+
notes: { a: L{|e| e.bar.index } }
|
68
|
+
)
|
69
|
+
|
70
|
+
3.times.map { notes(rabb) }.should == [
|
71
|
+
{ a: [0, 0] },
|
72
|
+
{ a: [1, 1] },
|
73
|
+
{ a: [2, 2] }
|
74
|
+
]
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'includes subdivisons in returned object' do
|
78
|
+
rabb = construct(subdivisions: 2)
|
79
|
+
rabb.next.subdivisions.should == 2
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'includes unit in returned object' do
|
83
|
+
rabb = construct(unit: 8)
|
84
|
+
rabb.next.unit.should == 8
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'includes generated unit in returned object' do
|
88
|
+
rabb = construct(unit: L{|e| 8 })
|
89
|
+
rabb.next.unit.should == 8
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'includes time_signature in returned object' do
|
93
|
+
rabb = construct(time_signature: "7/8")
|
94
|
+
rabb.next.time_signature.should == "7/8"
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'includes generated time_signature in returned object' do
|
98
|
+
rabb = construct(time_signature: L{|e| "7/8" })
|
99
|
+
rabb.next.time_signature.should == "7/8"
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'includes beat_structure in returned object' do
|
103
|
+
rabb = construct(beat_structure: [3, 2, 2])
|
104
|
+
rabb.next.beat_structure.should == [3, 2, 2]
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'includes generated beat_structure in returned object' do
|
108
|
+
rabb = construct(beat_structure: L{|e| [3, 2, 2] })
|
109
|
+
rabb.next.beat_structure.should == [3, 2, 2]
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'includes title in returned object' do
|
113
|
+
rabb = construct(title: "Hello")
|
114
|
+
rabb.next.title.should == "Hello"
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'includes generated title in returned object' do
|
118
|
+
rabb = construct(title: L{|e| "Hello" })
|
119
|
+
rabb.next.title.should == "Hello"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
describe Roborabb::Lilypond do
|
126
|
+
describe '#to_lilypond' do
|
127
|
+
def bar(attributes = {})
|
128
|
+
double("Bar", {
|
129
|
+
title: nil,
|
130
|
+
unit: 8,
|
131
|
+
notes: {hihat: [true]},
|
132
|
+
time_signature: "4/4",
|
133
|
+
beat_structure: [4, 4]
|
134
|
+
}.merge(attributes))
|
135
|
+
end
|
136
|
+
|
137
|
+
def output(generator, opts = {bars: 1})
|
138
|
+
formatter = described_class.new(generator.each, opts)
|
139
|
+
formatter.to_lilypond
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'outputs rests' do
|
143
|
+
generator = [bar(notes: {hihat: [false]})]
|
144
|
+
output(generator).should include("r")
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'outputs hihats' do
|
148
|
+
generator = [bar(notes: {hihat: [true]})]
|
149
|
+
output(generator).should include("hh")
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'calculates durations correctly to a maximum of four units' do
|
153
|
+
generator = [bar(unit: 32, notes: {hihat:
|
154
|
+
[true] +
|
155
|
+
[true] + [false] * 1 +
|
156
|
+
[true] + [false] * 2 +
|
157
|
+
[true] + [false] * 3 +
|
158
|
+
[true] + [false] * 4
|
159
|
+
})]
|
160
|
+
|
161
|
+
output(generator).should include("hh32 hh16 hh16. hh8 hh8 r32")
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'outputs kicks and snares' do
|
165
|
+
generator = [bar(unit: 4, notes: {
|
166
|
+
kick: [true, false],
|
167
|
+
snare: [false, true]
|
168
|
+
})]
|
169
|
+
output(generator).should include("bd4 sn4")
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'can output two notes at the same time' do
|
173
|
+
generator = [bar(unit: 4, notes: {kick: [true], snare: [true]})]
|
174
|
+
output(generator).should include("<bd sn>4")
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'can output a rest before a note' do
|
178
|
+
generator = [bar(unit: 8, notes: {hihat: [false, true]})]
|
179
|
+
output(generator).should include("r8 hh8")
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'includes lilypond preamble' do
|
183
|
+
lilypond = output([bar])
|
184
|
+
lilypond.should include("\\version")
|
185
|
+
lilypond.should include("\\new DrumStaff")
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'places hihats and kick/snare in different voices' do
|
189
|
+
generator = [bar(unit: 8, notes: {
|
190
|
+
hihat: [true, true],
|
191
|
+
kick: [true, false],
|
192
|
+
snare: [false, true]
|
193
|
+
})]
|
194
|
+
voices = output(generator).split("\\new DrumVoice")[1..-1]
|
195
|
+
voices[0].should include("hh8 hh8")
|
196
|
+
voices[0].should include("\\override Rest #'direction = #up")
|
197
|
+
voices[0].should include("\\stemUp")
|
198
|
+
voices[1].should include("bd8 sn8")
|
199
|
+
voices[1].should include("\\stemDown")
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'includes bar lines' do
|
203
|
+
generator = [
|
204
|
+
bar(notes: {hihat: [true] }),
|
205
|
+
bar(notes: {hihat: [false] }),
|
206
|
+
]
|
207
|
+
bars = output(generator, bars: 2).split('|')
|
208
|
+
bars[0].should include('hh')
|
209
|
+
bars[1].should include('r')
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'includes time signature changes per bar' do
|
213
|
+
generator = [
|
214
|
+
bar(time_signature: "1/8"),
|
215
|
+
bar(time_signature: "1/8"),
|
216
|
+
bar(time_signature: "1/4"),
|
217
|
+
]
|
218
|
+
bars = output(generator, bars: 3).split('|')
|
219
|
+
bars[0].should include(%(\\time 1/8))
|
220
|
+
bars[1].should_not include(%(\\time))
|
221
|
+
bars[2].should include(%(\\time 1/4))
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'includes beat structure changes per bar' do
|
225
|
+
generator = [
|
226
|
+
bar(beat_structure: [3, 2]),
|
227
|
+
bar(beat_structure: [3, 2]),
|
228
|
+
bar(beat_structure: [2, 3]),
|
229
|
+
]
|
230
|
+
bars = output(generator, bars: 3).split('|')
|
231
|
+
bars[0].should include(%(\\set Staff.beatStructure = #'(3 2)))
|
232
|
+
bars[1].should_not include(%(\\set Staff.beatStructure))
|
233
|
+
bars[2].should include(%(\\set Staff.beatStructure = #'(2 3)))
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'does not include beat structure if none provided' do
|
237
|
+
generator = [
|
238
|
+
bar(beat_structure: [3, 2]),
|
239
|
+
bar(beat_structure: nil)
|
240
|
+
]
|
241
|
+
bars = output(generator, bars: 2).split('|')
|
242
|
+
bars[0].should include(%(\\set Staff.beatStructure = #'(3 2)))
|
243
|
+
bars[1].should_not include(%(\\set Staff.beatStructure))
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'includes a final double bar line' do
|
247
|
+
output([bar]).should include(' \\bar "|."')
|
248
|
+
end
|
249
|
+
|
250
|
+
it "includes the final bar's title as the document title" do
|
251
|
+
lilypond = output([
|
252
|
+
bar(title: 'Wrong'),
|
253
|
+
bar(title: 'Hello'),
|
254
|
+
], bars: 2)
|
255
|
+
lilypond.should include(%(title = "Hello"))
|
256
|
+
lilypond.should_not include("Wrong")
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: roborabb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Xavier Shay
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-12-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &2156657040 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2156657040
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &2156656460 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2156656460
|
36
|
+
description: Algorithmically generate practice drum scores
|
37
|
+
email:
|
38
|
+
- hello@xaviershay.com
|
39
|
+
executables: []
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- spec/acceptance_spec.rb
|
44
|
+
- spec/unit_spec.rb
|
45
|
+
- lib/roborabb/bar.rb
|
46
|
+
- lib/roborabb/builder.rb
|
47
|
+
- lib/roborabb/core_ext.rb
|
48
|
+
- lib/roborabb/lilypond.rb
|
49
|
+
- lib/roborabb/version.rb
|
50
|
+
- lib/roborabb.rb
|
51
|
+
- README.md
|
52
|
+
- HISTORY.md
|
53
|
+
- Rakefile
|
54
|
+
- roborabb.gemspec
|
55
|
+
homepage: http://github.com/xaviershay/roborabb
|
56
|
+
licenses: []
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 1.8.6
|
76
|
+
signing_key:
|
77
|
+
specification_version: 3
|
78
|
+
summary: Algorithmically generate practice drum scores. Customize algorithms with
|
79
|
+
ruby with an archaeopteryx-inspired style, output to lilypond format.
|
80
|
+
test_files:
|
81
|
+
- spec/acceptance_spec.rb
|
82
|
+
- spec/unit_spec.rb
|