Step Sequencer
This is a Ruby tool to play mp3 files in a step sequencer.
It also handles polyrhythmic playback and building sounds using effects like loop, combine, slice, overlay, combine, gain, speed, pitch, and download.
Setup
Dependencies
Some external programs need to be installed:
mpg123 ffmpeg sox libsox-fmt-mp3
If using the 'download' command, then youtube-dl
is needed.
To run the tests, the program espeak
is also required.
Installing
gem install step_sequencer
require 'step_sequencer'
Configuration
The main point of config is to set the location that files are generated to.
This program creates a lot of mp3s and doesn't automatically delete any of them.
By default, the location is ./.step_sequencer/generated
which is a relative
path. That means it's dependent on which directory the command was run from.
To use an absolute path instead, set the STEP_SEQUENCER_OUTPUT_DIR
environment
variable.
REPL
There is a Ruby-based REPL that ships with the gem. Launch it from shell with:
step_sequencer repl
and type 'help' or 'quit'. It will show the names of some helper functions that have been made available. Still, the REPL assumes familiarity with the underlying Ruby API.
Ruby API
There are two main components: StepSequencer::SoundBuilder
and
StepSequencer::SoundPlayer
StepSequencer::SoundBuilder
This offers only one method, .build
, which is overloaded and dispatches to a
number of other classes (each of which is responsible for
a single effect).
note
The definitions of the default effects can be found in the source code at
lib/step_sequencer/sound_builder/default_effects/
. To add a custom effect, use one of the existing ones as a template and then add a reference to the class in theStepSequencer::SoundBuilder::EffectsComponents
hash.
Here is an example of using the builder. It takes a single input mp3 and creates 12 new ones, spanning the "equal temperament" tuning.
builder = StepSequencer::SoundBuilder
filenames = builder.build(
sources: ["middle_c.mp3"],
effect: :Scale,
args: [{scale: :equal_temperament}]
).first
# the :Scale effect returns a nested array (one array for each input source)
# so after calling .first, filenames refers to a regular array of 12 file paths.
The :Scale
effect also allows an inverse: true
key in args
which will
set all pitch change values to their inverse (creating a descending scale).
Right now there is only the equal temperament option, more may be added later.
Now say I want to apply a 150% gain to all these files:
# returns array of paths, one for each source
filenames = builder.build(
sources: filenames
effect: :Gain,
args: [{value: 1.5}]
)
Other builtin effects:
change speed
# returns array of paths, one for each source
filenames = builder.build(
sources: filenames,
effect: :Speed,
args: [{value: 0.5}]
)
change pitch
# returns array of paths, one for each source
filenames = builder.build(
sources: filenames,
effect: :Pitch,
args: [{value: 2}]
)
# By default this will adjust the speed so that only the pitch changes.
# However the `speed_correction: false` arg will prevent this.
loop
# returns array of paths, one for each source
filenames = builder.build(
sources: filenames,
effect: :Loop,
args: [{times: 1.5}]
)
slice
# returns array of paths, one for each source
filenames = builder.build(
sources: filenames,
effect: :Slice,
args: [{start_time: 0.5, end_time: 1}] # A 0.5 second slice
# start_pct and end_pct can be used for percentage-based slicing, e.g.
# {start_pct: 25, end_pct: 75} which will take a 50% slice from the middle.
)
combine
# returns single path
path = builder.build(
sources: filenames,
effect: :Combine,
args: [{filename: "foo.mp3"}], # filename arg is optional,
# and one will be auto-generated otherwise.
)
overlay
# returns single path
path = builder.build(
sources: filenames,
effect: :Overlay,
args: [{filename: "foo.mp3"}], # filename arg is optional,
# and one will be auto-generated otherwise.
)
As the above examples illustrate, #build
is always given an array of sources
(audio paths). effect
is always a symbol, and although it can be ommitted if
it's empty, args
is always a array, the signature of which is dependent on the
specific effects component's implementation.
StepSequencer::SoundPlayer
Playing sounds is a two part process. First, the player must be initialized, which is when the sounds are mapped to rows.
For example, say I want to plug in the sounds I created earlier using
StepSequencer::SoundBuilder#build
. I have 12 sounds and I want to give each
of them their own row in the sequencer. This is pretty easy to do:
player = StepSequencer::SoundPlayer.new(filenames)
After #initialize
, only one other method needs to get called, and that's play
.
As you might have expected by now, it's overloaded as well:
# This will play the ascending scale, there is 1 row for each note
player.play(
tempo: 240
string: <<-TXT
x _ _ _ _ _ _ _ _ _ _ _
_ x _ _ _ _ _ _ _ _ _ _
_ _ x _ _ _ _ _ _ _ _ _
_ _ _ x _ _ _ _ _ _ _ _ # comments can be added too
_ _ _ _ x _ _ _ _ _ _ _
_ _ _ _ _ x _ _ _ _ _ _
_ _ _ _ _ _ x _ _ _ _ _
_ _ _ _ _ _ _ x _ _ _ _
_ _ _ _ _ _ _ _ x _ _ _
_ _ _ _ _ _ _ _ _ x _ _
_ _ _ _ _ _ _ _ _ _ x _
_ _ _ _ _ _ _ _ _ _ _ x
TXT
)
To use something other than x
or _
, set the options :hit_char
and :rest_char
.
The following plays the same notes, but with nested arrays and the :matrix
option.
The hits/rests here are denoted by 1 and nil, respectively
player.play(
tempo: 240,
matrix: 0.upto(11).reduce([]) do |rows, i|
rows.push Array.new(12, nil)
rows[-1][i] = 1
rows
end
)
Some other notes:
- this intentionally doesn't validate that the rows are the same length. This is so that polyrhythms can be played.
- by default the rows are looped forever in a background thread. To stop, simply
player.stop
. To trigger playback for a limited time only, pass alimit
option which is an integer representing a number of total hits. I.e. if I wanted to play the aformentioned 12-step grid 4 times, I'd pass a limit of 48. - although there is no option to make
play
happen synchronously, just use something likesleep 0.5 while player.playing
- the tempo can be understood as BPM in quarter notes. So to get the same speed as 16th notes at 120 BPM, use a tempo of 480. The default is 120.
- The player isn't set up to be manipulated while playing. Use a new instance instead.
Todos
- precompile the grid into a single mp3. this will result in optimal playback
- support inlining effect directives into the grid (not just a separate build stage). Part of this could be supporting different note flags, such as whole notes, half notes, etc.
Tests
The tests are found in lib/step_sequencer/tests.rb. They are not traditional unit tests since the value cannot be automatically determined to be correct. Rather, it generates sounds and then plays them back for manual aural validation.
A couple small (1 second) mp3s are bundled with the gem and are used in the tests. To run the tests from the command line, use the executable included with the gem:
step_sequencer test
They can also be run from code: require 'step_sequencer'
then
StepSequencer::Tests.run
.
Downloading music from youtube
Bundled into this gem is youtube_audio_downloader, which provides a simple command to download audio from youtube:
YoutubeAudioDownloader.download(
"https://www.youtube.com/watch?v=Niuy_GqpU1s",
"~/Music",
"necrophagist_seven.mp3"
)