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