roborabb 0.0.1
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/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
|