step_sequencer 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +233 -0
- data/bin/step_sequencer +10 -0
- data/lib/step_sequencer/example.rb +14 -0
- data/lib/step_sequencer/refinements.rb +124 -0
- data/lib/step_sequencer/sound_builder/default_effects/combine.rb +25 -0
- data/lib/step_sequencer/sound_builder/default_effects/gain.rb +21 -0
- data/lib/step_sequencer/sound_builder/default_effects/loop.rb +47 -0
- data/lib/step_sequencer/sound_builder/default_effects/overlay.rb +35 -0
- data/lib/step_sequencer/sound_builder/default_effects/pitch.rb +40 -0
- data/lib/step_sequencer/sound_builder/default_effects/scale.rb +34 -0
- data/lib/step_sequencer/sound_builder/default_effects/slice.rb +46 -0
- data/lib/step_sequencer/sound_builder/default_effects/speed.rb +20 -0
- data/lib/step_sequencer/sound_builder/default_effects.rb +2 -0
- data/lib/step_sequencer/sound_builder/effects_component_protocol.rb +27 -0
- data/lib/step_sequencer/sound_builder.rb +27 -0
- data/lib/step_sequencer/sound_player.rb +132 -0
- data/lib/step_sequencer/test_assets/blip_1.mp3 +0 -0
- data/lib/step_sequencer/test_assets/blip_2.mp3 +0 -0
- data/lib/step_sequencer/tests/test_cases.rb +110 -0
- data/lib/step_sequencer/tests.rb +104 -0
- data/lib/step_sequencer.rb +33 -0
- data/lib/version.rb +3 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7f05e5833d38340144b70f118284273333692da7
|
4
|
+
data.tar.gz: f1f28455b5272f7dff4a7d2d3aa578bcf64fec49
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4b600c61efe15377b72893b484b020894e7aba4cd4e902da2f0d46d99146bf98aea810624b23d0d603cf4020724d95188ca8cd21eb1d3cfe3663cecbb44f7281
|
7
|
+
data.tar.gz: 170de1a63b8618181f7536fcfdcb8687602eaea3dd9e62ac07a3b68995188c32307f6edb2c432cf54542e5c734dcfa61be1049cbc124c29ea5d310f8ba294537
|
data/README.md
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
## Step Sequencer
|
2
|
+
|
3
|
+
#### About
|
4
|
+
|
5
|
+
This is a Ruby tool to play mp3 files in a step sequencer.
|
6
|
+
|
7
|
+
It also handles polyrhythmic playback and
|
8
|
+
|
9
|
+
#### Depedencies
|
10
|
+
|
11
|
+
Some external programs need to be installed (via `apt-get`, `brew`, `yum`, etc)
|
12
|
+
|
13
|
+
`mpg123 ffmpeg sox libsox-fmt-mp3`
|
14
|
+
|
15
|
+
To run the tests, the program `espeak` is also required.
|
16
|
+
|
17
|
+
#### Installing
|
18
|
+
|
19
|
+
```sh
|
20
|
+
gem install step_sequencer
|
21
|
+
```
|
22
|
+
|
23
|
+
```rb
|
24
|
+
require 'step_sequencer'
|
25
|
+
```
|
26
|
+
|
27
|
+
#### Usage
|
28
|
+
|
29
|
+
There are two main components: `StepSequencer::SoundBuilder` and
|
30
|
+
`StepSequencer::SoundPlayer`. A third component,
|
31
|
+
`StepSequencer::SoundAnalyser`, can also be useful.
|
32
|
+
|
33
|
+
##### 1. **`StepSequencer::SoundBuilder`**
|
34
|
+
|
35
|
+
There are number of `StepSequencer::SoundBuilder::EffectsComponents`
|
36
|
+
(this constant refers to a hash, the values of which are classes).
|
37
|
+
All of them inherit from and implement the protocol of
|
38
|
+
`StepSequencer::SoundBuilder::EffectsComponentProtocol`. To use a custom effect,
|
39
|
+
add the class to this list. Through the protocol, they're connected to the
|
40
|
+
`StepSequencer::SoundBuilder.build` method.
|
41
|
+
|
42
|
+
```rb
|
43
|
+
# returns nested array (array of generated sounds for each source)
|
44
|
+
builder = StepSequencer::SoundBuilder
|
45
|
+
filenames = builder.build(
|
46
|
+
sources: ["middle_c.mp3"],
|
47
|
+
effect: :Scale,
|
48
|
+
args: [{scale: :equal_temperament}]
|
49
|
+
)
|
50
|
+
```
|
51
|
+
|
52
|
+
The `:Scale` effect also allows an `inverse: true` key in `args` which will
|
53
|
+
set all pitch change values to their inverse (creating a descending scale).
|
54
|
+
|
55
|
+
Now say I want to apply a 150% gain to all these files:
|
56
|
+
|
57
|
+
```rb
|
58
|
+
# returns array of paths, one for each source
|
59
|
+
new_filenames = builder.build(
|
60
|
+
sources: filenames.shift,
|
61
|
+
effect: :Gain,
|
62
|
+
args: [{value: 1.5}]
|
63
|
+
)
|
64
|
+
```
|
65
|
+
|
66
|
+
Other builtin effects:
|
67
|
+
|
68
|
+
_change speed_ (returns array of paths, one for each source)
|
69
|
+
|
70
|
+
```rb
|
71
|
+
new_filenames = builder.build(
|
72
|
+
sources: new_filenames,
|
73
|
+
effect: :Speed,
|
74
|
+
args: [{value: 0.5}]
|
75
|
+
)
|
76
|
+
```
|
77
|
+
|
78
|
+
_change pitch_ (returns array of paths, one for each source)
|
79
|
+
|
80
|
+
```rb
|
81
|
+
new_filenames = builder.build(
|
82
|
+
sources: filenames,
|
83
|
+
effect: :Pitch,
|
84
|
+
args: [{value: 2}] # doubling pitch to account for 0.5 speed
|
85
|
+
)
|
86
|
+
# By default this will adjust the speed so that only the pitch changes.
|
87
|
+
# However the `speed_correction: false` arg prevents this.
|
88
|
+
```
|
89
|
+
|
90
|
+
_loop_ (returns array of paths, one for each source)
|
91
|
+
|
92
|
+
```rb
|
93
|
+
new_filenames = builder.build(
|
94
|
+
sources: filenames,
|
95
|
+
effect: :Loop,
|
96
|
+
args: [{times: 1.5}]
|
97
|
+
)
|
98
|
+
```
|
99
|
+
|
100
|
+
_slice_ (returns array of paths, one for each source)
|
101
|
+
|
102
|
+
```rb
|
103
|
+
new_filenames = builder.build(
|
104
|
+
sources: filenames,
|
105
|
+
effect: :Slice,
|
106
|
+
args: [{start_time: 0.5, end_time: 1}] # A 0.5 second slice
|
107
|
+
# start_pct and end_pct can be used for percentage-based slicing, e.g.
|
108
|
+
# {start_pct: 25, end_pct: 75} which will take a 50% slice from the middle.
|
109
|
+
)
|
110
|
+
```
|
111
|
+
|
112
|
+
_combine_ (returns single path)
|
113
|
+
|
114
|
+
```rb
|
115
|
+
new_filenames = builder.build(
|
116
|
+
sources: filenames,
|
117
|
+
effect: :Combine,
|
118
|
+
args: [{filename: "foo.mp3"}], # filename arg is optional,
|
119
|
+
# and one will be auto-generated otherwise.
|
120
|
+
)
|
121
|
+
`
|
122
|
+
|
123
|
+
_overlay_ (returns single path)
|
124
|
+
|
125
|
+
```rb
|
126
|
+
new_filenames = builder.build(
|
127
|
+
sources: filenames,
|
128
|
+
effect: :Overlay,
|
129
|
+
args: [{filename: "foo.mp3"}], # filename arg is optional,
|
130
|
+
# and one will be auto-generated otherwise.
|
131
|
+
)
|
132
|
+
`
|
133
|
+
|
134
|
+
As the above examples illustrate, `#build` is always given an array of sources
|
135
|
+
(audio paths). `effect` is always a symbol, and although it can be ommitted if
|
136
|
+
it's empty, `args` is always a array, the signature of which is dependent on the
|
137
|
+
specific effects component's implementation.
|
138
|
+
|
139
|
+
**2. `StepSequencer::SoundPlayer`**
|
140
|
+
|
141
|
+
Playing sounds is a two part process. First, the player must be initialized,
|
142
|
+
which is when the sounds are mapped to rows.
|
143
|
+
|
144
|
+
For example, say I want to plug in the sounds I created earlier using
|
145
|
+
`StepSequencer::SoundBuilder#build`. I have 12 sounds and I want to give each
|
146
|
+
of them their own row in the sequencer. This pretty easy to do:
|
147
|
+
|
148
|
+
```rb
|
149
|
+
player = StepSequencer::SoundPlayer.new(filenames)
|
150
|
+
```
|
151
|
+
|
152
|
+
After `#initialize`, only one other method needs to get called, and that's `play`.
|
153
|
+
As you might have expected by now, it's overloaded as well:
|
154
|
+
|
155
|
+
```rb
|
156
|
+
# This will play the descending scale, there is 1 row for each note
|
157
|
+
player.play(
|
158
|
+
tempo: 240
|
159
|
+
string: <<-TXT
|
160
|
+
x _ _ _ _ _ _ _ _ _ _ _
|
161
|
+
_ x _ _ _ _ _ _ _ _ _ _
|
162
|
+
_ _ x _ _ _ _ _ _ _ _ _
|
163
|
+
_ _ _ x _ _ _ _ _ _ _ _ # comments can be added too
|
164
|
+
_ _ _ _ x _ _ _ _ _ _ _
|
165
|
+
_ _ _ _ _ x _ _ _ _ _ _
|
166
|
+
_ _ _ _ _ _ x _ _ _ _ _
|
167
|
+
_ _ _ _ _ _ _ x _ _ _ _
|
168
|
+
_ _ _ _ _ _ _ _ x _ _ _
|
169
|
+
_ _ _ _ _ _ _ _ _ x _ _
|
170
|
+
_ _ _ _ _ _ _ _ _ _ x _
|
171
|
+
_ _ _ _ _ _ _ _ _ _ _ x
|
172
|
+
TXT
|
173
|
+
)
|
174
|
+
```
|
175
|
+
|
176
|
+
To use something other than `x` or `_`, use the environment variables
|
177
|
+
`STEP_SEQUENCER_GRID_HIT_CHAR` and `STEP_SEQUENCER_GRID_REST_CHAR`
|
178
|
+
|
179
|
+
```rb
|
180
|
+
# plays the same notes, but with nested arrays.
|
181
|
+
# the note/rest are denoted by 1 and nil, respectively
|
182
|
+
player.play(
|
183
|
+
tempo: 240,
|
184
|
+
matrix: 0.upto(11).reduce([]) do |rows, i|
|
185
|
+
rows.push Array.new(12, nil)
|
186
|
+
rows[-1][i] = 1
|
187
|
+
rows
|
188
|
+
end
|
189
|
+
)
|
190
|
+
```
|
191
|
+
|
192
|
+
Some other notes:
|
193
|
+
|
194
|
+
- this intentionally doesn't validate that the rows
|
195
|
+
are the same length. This is so that polyrhythms can be played.
|
196
|
+
- by default the rows are looped forever in a background thread. To stop, simply
|
197
|
+
`player.stop`. To trigger playback for a limited time only, pass a `limit` option
|
198
|
+
which is an integer representing a number of _total hits_. I.e. if I wanted to
|
199
|
+
play the aformentioned 12-step grid 4 times, I'd pass a limit of 48.
|
200
|
+
- although there is no option to make `play` happen synchronously, just use
|
201
|
+
something like `sleep 0.5 while player.playing`
|
202
|
+
- the tempo can be understood as BPM in quarter notes. So to get the same speed
|
203
|
+
as 16th notes at 120 BPM, use a tempo of 480. The default is 120.
|
204
|
+
- The player isn't configured to be manipulated while playing. Use a new instance instead.
|
205
|
+
|
206
|
+
#### Todos
|
207
|
+
|
208
|
+
- precompile the grid into a single mp3. this will result in optimal playback
|
209
|
+
but may be slightly difficult considering many sounds can be played at once,
|
210
|
+
and a single sound can be played over itself.
|
211
|
+
|
212
|
+
#### Tests
|
213
|
+
|
214
|
+
The tests are found in lib/step_sequencer/tests.rb. They are not traditional
|
215
|
+
unit tests since the value cannot be automatically determined to be correct.
|
216
|
+
Rather, it generates sounds and then plays them back for manual aural
|
217
|
+
validation.
|
218
|
+
|
219
|
+
A couple small (1 second) mp3s are bundled with the gem and are used in the tests.
|
220
|
+
To run the tests from the command line after installing the gem:
|
221
|
+
|
222
|
+
```rb
|
223
|
+
step_sequencer test
|
224
|
+
```
|
225
|
+
|
226
|
+
They are packaged with the distributed gem, so after `require 'step_sequencer'`
|
227
|
+
just use `StepSequencer::Tests.run`. Individual cases can also be run by calling
|
228
|
+
the class methods of `Builder` and `Player` in `StepSequencer::Tests::TestCases`.
|
229
|
+
|
230
|
+
There is one extra dependency required to use the tests, and that's the
|
231
|
+
`espeak-ruby` gem. which will indicate what sounds are playing.
|
232
|
+
This requires the external tool to be installed as well,
|
233
|
+
e.g. `brew install espeak` or `sudo apt-get install espeak`.
|
data/bin/step_sequencer
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
module StepSequencer::Refinements
|
2
|
+
|
3
|
+
# =============================================================================
|
4
|
+
# String#strip_heredoc
|
5
|
+
# -----------------------------------------------------------------------------
|
6
|
+
# Another one from active support, this lets heredocs be indented without
|
7
|
+
# effecting the underlying string
|
8
|
+
# =============================================================================
|
9
|
+
module StringStripHeredoc
|
10
|
+
def strip_heredoc
|
11
|
+
indent = scan(/^[ \t]*(?=\S)/).min.__send__(:size) || 0
|
12
|
+
gsub(/^[ \t]{#{indent}}/, '')
|
13
|
+
end
|
14
|
+
refine String do
|
15
|
+
include StepSequencer::Refinements::StringStripHeredoc
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# =============================================================================
|
21
|
+
# String#blank
|
22
|
+
# -----------------------------------------------------------------------------
|
23
|
+
# ActiveSupport provides a similar method for many classes,
|
24
|
+
# but from my experience the most useful is the String patch
|
25
|
+
# =============================================================================
|
26
|
+
module StringBlank
|
27
|
+
def blank?
|
28
|
+
/\A[[:space:]]*\z/ === self
|
29
|
+
end
|
30
|
+
refine String do
|
31
|
+
include StepSequencer::Refinements::StringBlank
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Object#yield_self
|
36
|
+
# -----------------------------------------------------------------------------
|
37
|
+
# is in Ruby since a v2.5 patch although there it's implemented on Kernel,
|
38
|
+
# which, being a module, makes it difficult to refine.
|
39
|
+
# Anyway, this is a backport; and it's a incredibly simply function
|
40
|
+
# =============================================================================
|
41
|
+
module ObjectYieldSelf
|
42
|
+
def yield_self(&blk)
|
43
|
+
blk&.call self
|
44
|
+
end
|
45
|
+
refine Object do
|
46
|
+
include StepSequencer::Refinements::ObjectYieldSelf
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# =============================================================================
|
51
|
+
# String#rational_eval
|
52
|
+
# -----------------------------------------------------------------------------
|
53
|
+
# a method I've written to help work with rational numbers.
|
54
|
+
# It evals a string containing math, but wraps all number values in a call
|
55
|
+
# It raises an error if the result is not a rational
|
56
|
+
# =============================================================================
|
57
|
+
module StringRationalEval
|
58
|
+
def rational_eval
|
59
|
+
result = eval <<-RB
|
60
|
+
Rational(#{
|
61
|
+
gsub(/\d[\d\.\_]*/) { |str| "Rational(#{str})"}
|
62
|
+
})
|
63
|
+
RB
|
64
|
+
result.is_a?(Rational) ? result : raise("#{result} is not Rational")
|
65
|
+
end
|
66
|
+
refine String do
|
67
|
+
include StepSequencer::Refinements::StringRationalEval
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# =============================================================================
|
72
|
+
# Symbol#call
|
73
|
+
# -----------------------------------------------------------------------------
|
74
|
+
# cryptic but immensely useful.
|
75
|
+
# It enables passing arguments with the symbol-to-proc shorthand, e.g.:
|
76
|
+
# [1,2,3].map(&:+.(1)) == [2,3,4]
|
77
|
+
# source: https://stackoverflow.com/a/23711606/2981429
|
78
|
+
# =============================================================================
|
79
|
+
module SymbolCall
|
80
|
+
def call(*args, &block)
|
81
|
+
->(caller, *rest) { caller.send(self, *rest, *args, &block) }
|
82
|
+
end
|
83
|
+
refine Symbol do
|
84
|
+
include StepSequencer::Refinements::SymbolCall
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# =============================================================================
|
89
|
+
# String#constantize
|
90
|
+
# -----------------------------------------------------------------------------
|
91
|
+
# comes from activesupport
|
92
|
+
# =============================================================================
|
93
|
+
module StringConstantize
|
94
|
+
def constantize
|
95
|
+
names = self.split("::".freeze)
|
96
|
+
# Trigger a built-in NameError exception including the ill-formed constant in the message.
|
97
|
+
Object.const_get(self) if names.empty?
|
98
|
+
# Remove the first blank element in case of '::ClassName' notation.
|
99
|
+
names.shift if names.size > 1 && names.first.empty?
|
100
|
+
names.inject(Object) do |constant, name|
|
101
|
+
if constant == Object
|
102
|
+
constant.const_get(name)
|
103
|
+
else
|
104
|
+
candidate = constant.const_get(name)
|
105
|
+
next candidate if constant.const_defined?(name, false)
|
106
|
+
next candidate unless Object.const_defined?(name)
|
107
|
+
# Go down the ancestors to check if it is owned directly. The check
|
108
|
+
# stops when we reach Object or the end of ancestors tree.
|
109
|
+
constant = constant.ancestors.inject(constant) do |const, ancestor|
|
110
|
+
break const if ancestor == Object
|
111
|
+
break ancestor if ancestor.const_defined?(name, false)
|
112
|
+
const
|
113
|
+
end
|
114
|
+
# owner is in Object, so raise
|
115
|
+
constant.const_get(name, false)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
refine String do
|
120
|
+
include StepSequencer::Refinements::StringConstantize
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
protocol = StepSequencer::SoundBuilder::EffectsComponentProtocol
|
2
|
+
|
3
|
+
class StepSequencer::SoundBuilder::DefaultEffects::Combine < protocol
|
4
|
+
|
5
|
+
# This combines the files one after another.
|
6
|
+
# To make them overlap, use Overlay
|
7
|
+
|
8
|
+
def self.build(sources:, filename: nil)
|
9
|
+
concat_cmd = "concat:#{sources.join("|")}"
|
10
|
+
outfile = filename || generate_outfile_path(sources)
|
11
|
+
system %{ffmpeg -y -i "#{concat_cmd}" -c copy #{outfile} 2> /dev/null }
|
12
|
+
outfile
|
13
|
+
end
|
14
|
+
|
15
|
+
# The following is an alternate script to combine:
|
16
|
+
# `sox #{sources.join(" ")} #{outfile}`
|
17
|
+
|
18
|
+
class << self
|
19
|
+
private
|
20
|
+
def generate_outfile_path(sources)
|
21
|
+
"#{output_dir}/#{SecureRandom.urlsafe_base64}.mp3"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
protocol = StepSequencer::SoundBuilder::EffectsComponentProtocol
|
2
|
+
|
3
|
+
class StepSequencer::SoundBuilder::DefaultEffects::Gain < protocol
|
4
|
+
|
5
|
+
def self.build(sources:, value:)
|
6
|
+
sources.map do |path|
|
7
|
+
outfile = build_outfile_path path, value
|
8
|
+
`ffmpeg -y -i "#{path}" -af "volume=#{value.to_f}" #{outfile} 2> /dev/null`
|
9
|
+
outfile
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
private
|
15
|
+
def build_outfile_path path, value
|
16
|
+
"#{output_dir}/#{SecureRandom.urlsafe_base64}.mp3"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
protocol = StepSequencer::SoundBuilder::EffectsComponentProtocol
|
2
|
+
|
3
|
+
class StepSequencer::SoundBuilder::DefaultEffects::Loop < protocol
|
4
|
+
|
5
|
+
def self.build(sources:, times:)
|
6
|
+
num_full_loops = times.to_i # rounds down
|
7
|
+
num_partial_loops = times - num_full_loops.to_f
|
8
|
+
outfiles = build_full_loops(sources, num_full_loops)
|
9
|
+
if num_partial_loops > 0
|
10
|
+
outfiles = build_partial_loops(sources, outfiles, num_partial_loops)
|
11
|
+
end
|
12
|
+
outfiles
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
private
|
17
|
+
def build_outfile_path
|
18
|
+
"#{output_dir}/#{SecureRandom.urlsafe_base64}.mp3"
|
19
|
+
end
|
20
|
+
def build_partial_loops(sources, outfiles, num_partial_loops)
|
21
|
+
outfiles.map.with_index do |outfile, idx|
|
22
|
+
source = sources[idx]
|
23
|
+
sliced = slice_source(source, num_partial_loops)
|
24
|
+
combine([outfile, sliced])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
def build_full_loops(sources, num_full_loops)
|
28
|
+
sources.map do |source|
|
29
|
+
combine(Array.new(num_full_loops, source))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
def slice_source(source, num_partial_loops)
|
33
|
+
builder.build(
|
34
|
+
sources: [source],
|
35
|
+
effect: :Slice,
|
36
|
+
args: [{start_pct: 0.0, end_pct: (num_partial_loops * 100) }]
|
37
|
+
)[0]
|
38
|
+
end
|
39
|
+
def combine(sources)
|
40
|
+
builder.build(
|
41
|
+
sources: sources,
|
42
|
+
effect: :Combine
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
protocol = StepSequencer::SoundBuilder::EffectsComponentProtocol
|
2
|
+
|
3
|
+
class StepSequencer::SoundBuilder::DefaultEffects::Overlay < protocol
|
4
|
+
|
5
|
+
def self.build(sources:, filename: nil)
|
6
|
+
# ensure_correct_sample_rates(sources)
|
7
|
+
outfile = filename || build_outfile_path
|
8
|
+
# `sox --combine mix #{sources.join(" ")} #{outfile}`
|
9
|
+
system <<-SH
|
10
|
+
ffmpeg -i #{sources.join(" -i ")} \
|
11
|
+
-filter_complex amerge -ac 2 -c:a libmp3lame -q:a 4 \
|
12
|
+
#{outfile} 2> /dev/null
|
13
|
+
SH
|
14
|
+
outfile
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
private
|
19
|
+
def build_outfile_path
|
20
|
+
"#{output_dir}/#{SecureRandom.urlsafe_base64}.mp3"
|
21
|
+
end
|
22
|
+
# def ensure_correct_sample_rates(sources)
|
23
|
+
# sources.each do |source|
|
24
|
+
# tmp_path = "#{SecureRandom.urlsafe_base64}.mp3"
|
25
|
+
# `sox -r 44.1k #{source} #{tmp_path}`
|
26
|
+
# unless File.exists?(tmp_path)
|
27
|
+
# raise StandardError, "an error occurred setting sample rate in Overlay"
|
28
|
+
# end
|
29
|
+
# `rm #{source}`
|
30
|
+
# `mv #{tmp_path} #{source}`
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
protocol = StepSequencer::SoundBuilder::EffectsComponentProtocol
|
2
|
+
|
3
|
+
class StepSequencer::SoundBuilder::DefaultEffects::Pitch < protocol
|
4
|
+
|
5
|
+
def self.build(sources:, value:, speed_correction: true)
|
6
|
+
sources.map &change_pitch(value, speed_correction)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.change_pitch(value, speed_correction)
|
10
|
+
->(source){
|
11
|
+
outfile = build_outfile_name(source, value)
|
12
|
+
cmd = <<-SH
|
13
|
+
ffmpeg -y -i #{source} -af \
|
14
|
+
asetrate=44100*#{value} \
|
15
|
+
#{outfile} \
|
16
|
+
2> /dev/null
|
17
|
+
SH
|
18
|
+
system cmd
|
19
|
+
return outfile unless speed_correction
|
20
|
+
outfile_with_correct_speed = correct_speed(outfile, value)
|
21
|
+
outfile_with_correct_speed
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
private
|
27
|
+
def build_outfile_name(source, value)
|
28
|
+
"#{output_dir}/#{SecureRandom.urlsafe_base64}.mp3"
|
29
|
+
end
|
30
|
+
def correct_speed(outfile, pitch_change_value)
|
31
|
+
inverse_value = Rational(1) / Rational(pitch_change_value)
|
32
|
+
builder.build(
|
33
|
+
sources: [outfile],
|
34
|
+
effect: :Speed,
|
35
|
+
args: [value: inverse_value]
|
36
|
+
).shift
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
protocol = StepSequencer::SoundBuilder::EffectsComponentProtocol
|
2
|
+
|
3
|
+
class StepSequencer::SoundBuilder::DefaultEffects::Scale < protocol
|
4
|
+
|
5
|
+
using StepSequencer.refinement(:SymbolCall)
|
6
|
+
using StepSequencer.refinement(:StringRationalEval)
|
7
|
+
using StepSequencer.refinement(:ObjectYieldSelf)
|
8
|
+
|
9
|
+
# Each returns an array of floats, representing pitch change from original
|
10
|
+
Scales = {
|
11
|
+
equal_temperament: begin
|
12
|
+
twelth_root_of_two = "2 ** (1 / 12)".rational_eval
|
13
|
+
1.upto(12).reduce([twelth_root_of_two]) do |memo|
|
14
|
+
memo.concat([memo[-1] * twelth_root_of_two])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
}
|
18
|
+
|
19
|
+
def self.build(sources:, scale:, inverse: false, speed_correction: true)
|
20
|
+
pitch_changes = Scales.fetch(scale).yield_self do |notes|
|
21
|
+
inverse ? notes.map(&Rational(1).method(:/)) : notes
|
22
|
+
end
|
23
|
+
sources.map do |source|
|
24
|
+
pitch_changes.flat_map do |pitch_change|
|
25
|
+
builder.build(
|
26
|
+
sources: [source],
|
27
|
+
effect: :Pitch,
|
28
|
+
args: [value: pitch_change, speed_correction: speed_correction]
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
protocol = StepSequencer::SoundBuilder::EffectsComponentProtocol
|
2
|
+
|
3
|
+
class StepSequencer::SoundBuilder::DefaultEffects::Slice < protocol
|
4
|
+
|
5
|
+
def self.build(sources:, start_pct: nil, end_pct: nil, start_time: nil, end_time: nil)
|
6
|
+
sources.map do |source|
|
7
|
+
len = get_audio_length(source)
|
8
|
+
start_time ||= calc_start_time(source, len, start_pct)
|
9
|
+
end_time ||= calc_end_time(source, len, end_pct)
|
10
|
+
diff = (end_time - start_time).round(6)
|
11
|
+
outfile = build_outfile_path
|
12
|
+
`sox #{source} #{outfile} trim #{start_time} #{diff} 2> /dev/null`
|
13
|
+
outfile
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
private
|
19
|
+
def build_outfile_path
|
20
|
+
"#{output_dir}/#{SecureRandom.urlsafe_base64}.mp3"
|
21
|
+
end
|
22
|
+
def get_audio_length(source)
|
23
|
+
raise(
|
24
|
+
StandardError,
|
25
|
+
"#{source} doesn't exist (can't slice)"
|
26
|
+
) unless File.exists?(source)
|
27
|
+
`soxi -D #{source}`.to_f
|
28
|
+
end
|
29
|
+
def calc_start_time(source, len, start_pct)
|
30
|
+
raise(
|
31
|
+
StandardError,
|
32
|
+
"one of start_pct or start_time needs to be passed to Slice"
|
33
|
+
) unless start_pct
|
34
|
+
start_pct_decimal = start_pct.to_f / 100.0
|
35
|
+
len * start_pct_decimal
|
36
|
+
end
|
37
|
+
def calc_end_time(sourse, len, end_pct)
|
38
|
+
raise(
|
39
|
+
StandardError,
|
40
|
+
"one of end_pct or end_time needs to be passed to Slice"
|
41
|
+
) unless end_pct
|
42
|
+
end_pct_decimal = end_pct.to_f / 100.0
|
43
|
+
len * end_pct_decimal
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
protocol = StepSequencer::SoundBuilder::EffectsComponentProtocol
|
2
|
+
|
3
|
+
class StepSequencer::SoundBuilder::DefaultEffects::Speed < protocol
|
4
|
+
|
5
|
+
def self.build(sources:, value:)
|
6
|
+
sources.map do |path|
|
7
|
+
outfile = build_outfile_path path, value
|
8
|
+
`sox #{path} #{outfile} tempo #{value.to_f} 2> /dev/null`
|
9
|
+
outfile
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
private
|
15
|
+
def build_outfile_path path, value
|
16
|
+
"#{output_dir}/#{SecureRandom.urlsafe_base64}.mp3"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class StepSequencer::SoundBuilder::EffectsComponentProtocol
|
2
|
+
|
3
|
+
# receives dispatch from SoundBuilder.build
|
4
|
+
def self.build(sources:, args:)
|
5
|
+
raise "
|
6
|
+
ERROR.
|
7
|
+
Something inheriting from #{self.class} didn't implement #build.
|
8
|
+
"
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
# Helper method to call other effects
|
16
|
+
def builder
|
17
|
+
StepSequencer::SoundBuilder
|
18
|
+
end
|
19
|
+
|
20
|
+
# Any created files should be placed in here (see sound_builder.rb)
|
21
|
+
def output_dir
|
22
|
+
builder::OutputDir
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class StepSequencer::SoundBuilder
|
2
|
+
|
3
|
+
# Check the ENV config for an output dir, otherwise use a default one
|
4
|
+
OutputDir = ENV.fetch(
|
5
|
+
"STEP_SEQUENCER_OUTPUT_DIR",
|
6
|
+
"./.step_sequencer/generated"
|
7
|
+
).tap do |path|
|
8
|
+
`mkdir -p #{path}`
|
9
|
+
raise(
|
10
|
+
StandardError,
|
11
|
+
"#{path} dir couldn't be created/found. Maybe create it manually."
|
12
|
+
) unless File.directory?(path)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.build(sources:, effect:, args: [{}])
|
16
|
+
effect_class = effects_components[effect]
|
17
|
+
effect_class.build({sources: sources}.merge *args)
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
private
|
22
|
+
def effects_components
|
23
|
+
StepSequencer::SoundBuilder::EffectsComponents
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
class StepSequencer::SoundPlayer
|
2
|
+
|
3
|
+
using StepSequencer.refinement(:StringBlank)
|
4
|
+
|
5
|
+
HitChar = ENV.fetch("STEP_SEQUENCER_GRID_HIT_CHAR", "x")
|
6
|
+
RestChar = ENV.fetch("STEP_SEQUENCER_GRID_REST_CHAR", "_")
|
7
|
+
|
8
|
+
if [HitChar, RestChar].any? &:blank?
|
9
|
+
raise StandardError, "HitChar or RestChar cannot be just whitespace"
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :playing
|
13
|
+
|
14
|
+
def initialize(sources)
|
15
|
+
@sources = sources
|
16
|
+
reset_state
|
17
|
+
end
|
18
|
+
|
19
|
+
def play(tempo: 120, string: nil, matrix: nil, limit: nil)
|
20
|
+
@limit = limit
|
21
|
+
if @playing
|
22
|
+
raise( StandardError,
|
23
|
+
"A sound player received #play when it was not in a stopped state.
|
24
|
+
Use multiple instances instead"
|
25
|
+
)
|
26
|
+
end
|
27
|
+
if matrix
|
28
|
+
play_from_matrix(tempo: tempo, matrix: matrix)
|
29
|
+
else
|
30
|
+
play_from_string(tempo: tempo, string: string)
|
31
|
+
end
|
32
|
+
@playing = true
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop
|
36
|
+
reset_state
|
37
|
+
@thread&.kill
|
38
|
+
end
|
39
|
+
|
40
|
+
def reset_state
|
41
|
+
@thread = nil
|
42
|
+
@playing = false # It can't be called multiple times at once.
|
43
|
+
# Use multiple instances instead.
|
44
|
+
# There's a check to precaution against this.
|
45
|
+
@row_lengths = []
|
46
|
+
@active_steps = []
|
47
|
+
@steps_played = 0
|
48
|
+
@limit = nil # an upper limit for steps_played, defaults to no limit
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def build_matrix_from_string(string)
|
54
|
+
string.tr(" ", '').split("\n").map(&:chars).map do |chars|
|
55
|
+
chars.map do |char|
|
56
|
+
if char == hit_char then 1
|
57
|
+
elsif char == rest_char then nil
|
58
|
+
else
|
59
|
+
raise( StandardError,
|
60
|
+
"
|
61
|
+
Error playing from string. Found char #{char} which is not
|
62
|
+
one of '#{hit_char}' or '#{rest_char}'.
|
63
|
+
"
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def play_from_string(tempo:, string:)
|
71
|
+
raise(
|
72
|
+
StandardError,
|
73
|
+
"one of :string or :matrix must be provided for SoundPlayer#play"
|
74
|
+
) if !string
|
75
|
+
matrix = build_matrix_from_string(string)
|
76
|
+
play_from_matrix tempo: tempo, matrix: matrix
|
77
|
+
end
|
78
|
+
|
79
|
+
def play_from_matrix(tempo:, matrix:)
|
80
|
+
init_matrix_state matrix
|
81
|
+
rest_time = calculate_rest_time(tempo)
|
82
|
+
@thread = Thread.new do
|
83
|
+
loop do
|
84
|
+
if @limit && (@steps_played >= @limit)
|
85
|
+
stop # Thread kills itself, no need to break from the loop
|
86
|
+
end
|
87
|
+
matrix.each_with_index do |row, row_idx|
|
88
|
+
play_next_step_in_row row, row_idx
|
89
|
+
end
|
90
|
+
@steps_played += 1
|
91
|
+
sleep rest_time
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def play_next_step_in_row row, row_idx
|
97
|
+
Thread.new do
|
98
|
+
row_length = @row_lengths[row_idx]
|
99
|
+
active_step = @active_steps[row_idx]
|
100
|
+
Thread.stop unless active_step
|
101
|
+
step_content = row[active_step]
|
102
|
+
if step_content
|
103
|
+
path = @sources[row_idx]
|
104
|
+
Thread.new { `mpg123 #{path} 2> /dev/null` }
|
105
|
+
end
|
106
|
+
@active_steps[row_idx] += 1
|
107
|
+
if @active_steps[row_idx] >= row_length
|
108
|
+
@active_steps[row_idx] = 0
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def calculate_rest_time(tempo)
|
114
|
+
# Tempo is seen as quarter notes, i.e. at 120 BPM there's 0.5 seconds
|
115
|
+
# between steps
|
116
|
+
60.0 / tempo.to_f
|
117
|
+
end
|
118
|
+
|
119
|
+
def init_matrix_state(matrix)
|
120
|
+
@row_lengths = matrix.map(&:length)
|
121
|
+
@active_steps = Array.new((matrix.length), 0)
|
122
|
+
end
|
123
|
+
|
124
|
+
def hit_char
|
125
|
+
self.class::HitChar
|
126
|
+
end
|
127
|
+
|
128
|
+
def rest_char
|
129
|
+
self.class::RestChar
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
Binary file
|
Binary file
|
@@ -0,0 +1,110 @@
|
|
1
|
+
class StepSequencer::Tests::TestCases
|
2
|
+
|
3
|
+
class Player
|
4
|
+
extend StepSequencer::Tests::TestCaseHelpers
|
5
|
+
|
6
|
+
|
7
|
+
def self.play_a_simple_grid_from_string
|
8
|
+
player = build_player %w{blip_1 blip_1}.map(&method(:asset_path))
|
9
|
+
player.play(
|
10
|
+
tempo: 240,
|
11
|
+
limit: 16,
|
12
|
+
string: <<-TXT,
|
13
|
+
x _ x _
|
14
|
+
_ _ x _
|
15
|
+
TXT
|
16
|
+
)
|
17
|
+
sleep 0.5 while player.playing
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.play_a_polyrhythmic_string
|
21
|
+
player = build_player %w{blip_1 blip_1}.map(&method(:asset_path))
|
22
|
+
player.play(
|
23
|
+
tempo: 240,
|
24
|
+
limit: 16,
|
25
|
+
string: <<-TXT,
|
26
|
+
x _ _
|
27
|
+
x _ _ _
|
28
|
+
TXT
|
29
|
+
)
|
30
|
+
sleep 0.5 while player.playing
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class Builder
|
36
|
+
|
37
|
+
extend StepSequencer::Tests::TestCaseHelpers
|
38
|
+
|
39
|
+
def self.build_c_major_chord
|
40
|
+
scale = equal_temperament_notes_with_speed_correction[0]
|
41
|
+
result_path = builder.build(
|
42
|
+
sources: scale.values_at(0, 4, 7),
|
43
|
+
effect: :Overlay
|
44
|
+
)
|
45
|
+
[result_path]
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.build_f_sharp_major_chord
|
49
|
+
scale = equal_temperament_notes_with_speed_correction[0]
|
50
|
+
result_path = builder.build(
|
51
|
+
sources: scale.values_at(1, 6, 10),
|
52
|
+
effect: :Overlay
|
53
|
+
)
|
54
|
+
[result_path]
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.loop_a_sound_4_point_5_times
|
58
|
+
builder.build(
|
59
|
+
sources: [asset_path("blip_1")],
|
60
|
+
effect: :Loop,
|
61
|
+
args: [times: 4.5]
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.equal_temperament_notes_combined
|
66
|
+
scale_sources = equal_temperament_notes_with_speed_correction[0]
|
67
|
+
result_path = builder.build(
|
68
|
+
sources: scale_sources,
|
69
|
+
effect: :Combine
|
70
|
+
)
|
71
|
+
[result_path]
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.equal_temperament_notes_with_speed_correction
|
75
|
+
builder.build(
|
76
|
+
sources: [asset_path("blip_1")],
|
77
|
+
effect: :Scale,
|
78
|
+
args: [scale: :equal_temperament]
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.equal_temperament_with_no_speed_correction
|
83
|
+
builder.build(
|
84
|
+
sources: [asset_path("blip_1")],
|
85
|
+
effect: :Scale,
|
86
|
+
args: [scale: :equal_temperament, speed_correction: false]
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.inverse_equal_temperament
|
91
|
+
builder.build(
|
92
|
+
sources: [asset_path("blip_1")],
|
93
|
+
effect: :Scale,
|
94
|
+
args: [scale: :equal_temperament, inverse: true]
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.increase_gain
|
99
|
+
[0.25, 0.5, 0.75, 1, 1.5, 3, 5, 10].map do |val|
|
100
|
+
builder.build(
|
101
|
+
sources: [asset_path("blip_1")],
|
102
|
+
effect: :Gain,
|
103
|
+
args: [value: val]
|
104
|
+
)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'espeak'
|
2
|
+
require 'method_source'
|
3
|
+
|
4
|
+
# =============================================================================
|
5
|
+
# A custom test runner. The tests themselves are in test_cases.rb
|
6
|
+
# Usage: StepSequencer::Tests.run
|
7
|
+
# The private method run_test_collection can also be used; it's more modular.
|
8
|
+
# =============================================================================
|
9
|
+
|
10
|
+
class StepSequencer::Tests
|
11
|
+
|
12
|
+
# Runs all the test cases, speaking the method name and playing the result.
|
13
|
+
#
|
14
|
+
# Returns the results from the tests, for manual inspection from Pry
|
15
|
+
#
|
16
|
+
def self.run
|
17
|
+
cleanup
|
18
|
+
run_test_collection(builder_tests) do |fn_name, test_case|
|
19
|
+
result = run_test_case(builder_tests, fn_name, test_case)
|
20
|
+
result.tap &method(:play_sounds)
|
21
|
+
end
|
22
|
+
run_test_collection(player_tests) do |fn_name, test_case|
|
23
|
+
run_test_case(player_tests, fn_name, test_case)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# shared 'around' hook for tests
|
28
|
+
def self.run_test_case(test_class, fn_name, test_case)
|
29
|
+
speak_fn_name fn_name
|
30
|
+
puts fn_name
|
31
|
+
test_class.method(fn_name).source.display
|
32
|
+
test_case.call
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
private
|
37
|
+
|
38
|
+
def builder_tests
|
39
|
+
StepSequencer::Tests::TestCases::Builder
|
40
|
+
end
|
41
|
+
def player_tests
|
42
|
+
StepSequencer::Tests::TestCases::Player
|
43
|
+
end
|
44
|
+
|
45
|
+
def speak_fn_name fn_name
|
46
|
+
say fn_name.to_s.gsub("_", " ")
|
47
|
+
end
|
48
|
+
|
49
|
+
def cleanup
|
50
|
+
dir = StepSequencer::SoundBuilder::OutputDir
|
51
|
+
Dir.glob("#{dir}/*.mp3").each &File.method(:delete)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Runs all the methods in a class, optionally filtered via :only and
|
55
|
+
# :accept options (arrays of symbols referencing methods of klass).
|
56
|
+
#
|
57
|
+
# If a block is provided, then it is passed the name as a symbol and
|
58
|
+
# the test case as a proc. The proc will need to be called manually
|
59
|
+
# from the block, and the block should return the result.
|
60
|
+
# This allows a before/after hook to be inserted.
|
61
|
+
# With no given block, the test case is automatically run.
|
62
|
+
#
|
63
|
+
# Returns a hash mapping test case names (symbols) to results
|
64
|
+
#
|
65
|
+
def run_test_collection(klass, only: nil, except: nil, &blk)
|
66
|
+
klass.methods(false).reduce({}) do |memo, fn_name|
|
67
|
+
next memo if only && !only.include?(fn_name)
|
68
|
+
next memo if except && except.include?(fn_name)
|
69
|
+
memo[fn_name] = if blk
|
70
|
+
blk.call(fn_name, ->{klass.send fn_name})
|
71
|
+
else
|
72
|
+
klass.send(fn_name)
|
73
|
+
end
|
74
|
+
memo
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def say(phrase)
|
79
|
+
ESpeak::Speech.new(phrase).speak
|
80
|
+
end
|
81
|
+
def play_sounds(sounds, tempo: 800)
|
82
|
+
sleep_time = 60.0 / tempo.to_f
|
83
|
+
sounds.flatten.each do |path|
|
84
|
+
`mpg123 #{path} 2> /dev/null`
|
85
|
+
sleep sleep_time
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Helpers made available to test cases (if they include the module)
|
91
|
+
module TestCaseHelpers
|
92
|
+
private
|
93
|
+
def asset_path(name)
|
94
|
+
Gem.find_files("step_sequencer/test_assets/#{name}.mp3")[0]
|
95
|
+
end
|
96
|
+
def builder
|
97
|
+
StepSequencer::SoundBuilder
|
98
|
+
end
|
99
|
+
def build_player(sources)
|
100
|
+
StepSequencer::SoundPlayer.new sources
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'byebug'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
Thread.abort_on_exception = true
|
5
|
+
|
6
|
+
# only top-level constant defined by the gem
|
7
|
+
class StepSequencer; end
|
8
|
+
|
9
|
+
# Require the refinements file first
|
10
|
+
refinements_file = Gem.find_files("step_sequencer/refinements.rb").shift
|
11
|
+
require refinements_file
|
12
|
+
|
13
|
+
# Add a little util method to access the refinements from other files.
|
14
|
+
class StepSequencer
|
15
|
+
def self.refinement(name)
|
16
|
+
StepSequencer::Refinements.const_get name
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Require all ruby files in lib/step_sequencer, ordered by depth
|
21
|
+
# This loads the refinements file again but it's no big deal
|
22
|
+
Gem.find_files("step_sequencer/**/*.rb")
|
23
|
+
.sort_by { |path| path.count "/" }
|
24
|
+
.each &method(:require)
|
25
|
+
|
26
|
+
# Add the default effects to the sound builder
|
27
|
+
StepSequencer::SoundBuilder.class_exec do
|
28
|
+
self::EffectsComponents = self::DefaultEffects.constants
|
29
|
+
.reduce({}) do |memo, name|
|
30
|
+
memo.tap { memo[name] = self::DefaultEffects.const_get name }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
data/lib/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: step_sequencer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- max pleaner
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-06-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.19'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.19'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: method_source
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.8'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.8'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: espeak-ruby
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1'
|
55
|
+
description: ''
|
56
|
+
email: maxpleaner@gmail.com
|
57
|
+
executables:
|
58
|
+
- step_sequencer
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- README.md
|
63
|
+
- bin/step_sequencer
|
64
|
+
- lib/step_sequencer.rb
|
65
|
+
- lib/step_sequencer/example.rb
|
66
|
+
- lib/step_sequencer/refinements.rb
|
67
|
+
- lib/step_sequencer/sound_builder.rb
|
68
|
+
- lib/step_sequencer/sound_builder/default_effects.rb
|
69
|
+
- lib/step_sequencer/sound_builder/default_effects/combine.rb
|
70
|
+
- lib/step_sequencer/sound_builder/default_effects/gain.rb
|
71
|
+
- lib/step_sequencer/sound_builder/default_effects/loop.rb
|
72
|
+
- lib/step_sequencer/sound_builder/default_effects/overlay.rb
|
73
|
+
- lib/step_sequencer/sound_builder/default_effects/pitch.rb
|
74
|
+
- lib/step_sequencer/sound_builder/default_effects/scale.rb
|
75
|
+
- lib/step_sequencer/sound_builder/default_effects/slice.rb
|
76
|
+
- lib/step_sequencer/sound_builder/default_effects/speed.rb
|
77
|
+
- lib/step_sequencer/sound_builder/effects_component_protocol.rb
|
78
|
+
- lib/step_sequencer/sound_player.rb
|
79
|
+
- lib/step_sequencer/test_assets/blip_1.mp3
|
80
|
+
- lib/step_sequencer/test_assets/blip_2.mp3
|
81
|
+
- lib/step_sequencer/tests.rb
|
82
|
+
- lib/step_sequencer/tests/test_cases.rb
|
83
|
+
- lib/version.rb
|
84
|
+
homepage: http://github.com/maxpleaner/step_sequencer
|
85
|
+
licenses:
|
86
|
+
- MIT
|
87
|
+
metadata: {}
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.3'
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 2.6.11
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project:
|
104
|
+
rubygems_version: 2.6.11
|
105
|
+
signing_key:
|
106
|
+
specification_version: 4
|
107
|
+
summary: step sequencer (music tool)
|
108
|
+
test_files: []
|