beats 1.2.4 → 1.2.5
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/LICENSE +1 -1
- data/README.markdown +24 -4
- data/Rakefile +1 -1
- data/bin/beats +10 -15
- data/ext/mkrf_conf.rb +28 -0
- data/lib/beats.rb +12 -76
- data/lib/beats/audioengine.rb +163 -0
- data/lib/beats/audioutils.rb +74 -0
- data/lib/beats/beatsrunner.rb +76 -0
- data/lib/beats/kit.rb +187 -0
- data/lib/beats/pattern.rb +88 -0
- data/lib/beats/song.rb +162 -0
- data/lib/beats/songoptimizer.rb +162 -0
- data/lib/beats/songparser.rb +217 -0
- data/lib/beats/track.rb +57 -0
- data/lib/wavefile/cachingwriter.rb +1 -1
- data/test/audioengine_test.rb +5 -5
- data/test/cachingwriter_test.rb +1 -1
- data/test/fixtures/valid/foo.txt +18 -0
- data/test/includes.rb +2 -9
- data/test/integration_test.rb +20 -20
- data/test/kit_test.rb +28 -28
- data/test/pattern_test.rb +13 -13
- data/test/song_test.rb +23 -23
- data/test/songoptimizer_test.rb +25 -25
- data/test/songparser_test.rb +11 -11
- data/test/sounds/bass.wav +0 -0
- data/test/sounds/bass2.wav +0 -0
- data/test/track_test.rb +12 -12
- metadata +27 -22
- data/lib/audioengine.rb +0 -161
- data/lib/audioutils.rb +0 -73
- data/lib/kit.rb +0 -185
- data/lib/pattern.rb +0 -86
- data/lib/song.rb +0 -160
- data/lib/songoptimizer.rb +0 -160
- data/lib/songparser.rb +0 -216
- data/lib/track.rb +0 -55
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4f2f7351421971cb29e95868abde34acaa5ab6b2
|
4
|
+
data.tar.gz: ba692d9e94e5cac568eff994493a45b4739ee2ac
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 23dd31bfc7e19dcf52dcc573e054f36b46f93b4cd5d5aaa9e9077d9a9227082ddf42b4781f9564af1ab54fd70de1758116e8cf94c6189ca1bbf3f7426628b8dc
|
7
|
+
data.tar.gz: 19d77737f3a74672d0d73267ba673ecebf99c46c8af487d75762ee8956aab9e034d888d39707e1d6ef10cb6bdb3fd42f499b838b6e032290db664a2476ec5a68
|
data/LICENSE
CHANGED
data/README.markdown
CHANGED
@@ -33,16 +33,19 @@ And [here's what it sounds like](http://beatsdrummachine.com/media/beat.mp3) aft
|
|
33
33
|
Current Status
|
34
34
|
--------------
|
35
35
|
|
36
|
-
The latest stable version of Beats is 1.2.
|
36
|
+
The latest stable version of Beats is 1.2.5, released on December 31, 2013. This release includes these improvements:
|
37
37
|
|
38
|
-
*
|
39
|
-
*
|
38
|
+
* Tracks that start with a `|` no longer cause an error in Ruby 2.0.0 and 2.1.0.
|
39
|
+
* Additional Wave file formats can now be used as samples, due to upgrading to [WaveFile 0.6.0](http://wavefilegem.com) behind the scenes:
|
40
|
+
* 24-bit PCM
|
41
|
+
* 32-bit IEEE Float
|
42
|
+
* 64-bit IEEE Float
|
40
43
|
|
41
44
|
|
42
45
|
Installation
|
43
46
|
------------
|
44
47
|
|
45
|
-
To install the latest stable version (1.2.
|
48
|
+
To install the latest stable version (1.2.5) from [rubygems.org](http://rubygems.org/gems/beats), run the following from the command line:
|
46
49
|
|
47
50
|
gem install beats
|
48
51
|
|
@@ -61,6 +64,23 @@ Beats runs from the command-line. Run `beats -h` to see the available options. F
|
|
61
64
|
The Beats wiki also has a [Getting Started](https://github.com/jstrait/beats/wiki/Getting-Started) tutorial which shows how to create an example beat from scratch.
|
62
65
|
|
63
66
|
|
67
|
+
Local Development
|
68
|
+
-----------------
|
69
|
+
|
70
|
+
First, install the required dependencies:
|
71
|
+
|
72
|
+
bundle install
|
73
|
+
|
74
|
+
To run Beats locally, use `bundle exec` and run `bin/beats`, to avoid using any installed gem executable. For example:
|
75
|
+
|
76
|
+
bundle exec bin/beats -v
|
77
|
+
|
78
|
+
To run the tests:
|
79
|
+
|
80
|
+
bundle exec rake test
|
81
|
+
|
82
|
+
|
83
|
+
|
64
84
|
Found a Bug? Have a Suggestion? Want to Contribute?
|
65
85
|
---------------------------------------------------
|
66
86
|
|
data/Rakefile
CHANGED
data/bin/beats
CHANGED
@@ -2,20 +2,15 @@
|
|
2
2
|
|
3
3
|
start_time = Time.now
|
4
4
|
|
5
|
-
$:.unshift File.dirname(__FILE__) + "
|
5
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
6
6
|
require "optparse"
|
7
7
|
require "yaml"
|
8
|
+
require "syck"
|
8
9
|
require "wavefile"
|
9
|
-
require "
|
10
|
-
require "
|
11
|
-
|
12
|
-
|
13
|
-
require "lib/kit"
|
14
|
-
require "lib/pattern"
|
15
|
-
require "lib/song"
|
16
|
-
require "lib/songoptimizer"
|
17
|
-
require "lib/songparser"
|
18
|
-
require "lib/track"
|
10
|
+
require "beats"
|
11
|
+
require "wavefile/cachingwriter"
|
12
|
+
|
13
|
+
include Beats
|
19
14
|
|
20
15
|
USAGE_INSTRUCTIONS = ""
|
21
16
|
YAML::ENGINE.yamler = 'syck' if defined?(YAML::ENGINE)
|
@@ -39,7 +34,7 @@ def parse_options()
|
|
39
34
|
end
|
40
35
|
|
41
36
|
opts.on('-v', '--version', "Display version number and exit") do
|
42
|
-
puts "
|
37
|
+
puts "Beats Drum Machine #{Beats::VERSION}"
|
43
38
|
exit
|
44
39
|
end
|
45
40
|
|
@@ -69,7 +64,7 @@ def print_error(error, input_file_name)
|
|
69
64
|
puts "An error occured while generating sound for '#{input_file_name}':\n"
|
70
65
|
puts " #{error}\n"
|
71
66
|
else
|
72
|
-
puts "An unexpected error occured while running
|
67
|
+
puts "An unexpected error occured while running Beats Drum Machine:"
|
73
68
|
puts " #{error}\n"
|
74
69
|
end
|
75
70
|
end
|
@@ -79,11 +74,11 @@ begin
|
|
79
74
|
input_file_name = ARGV[0]
|
80
75
|
output_file_name = ARGV[1]
|
81
76
|
|
82
|
-
beats =
|
77
|
+
beats = BeatsRunner.new(input_file_name, output_file_name, options)
|
83
78
|
|
84
79
|
output = beats.run()
|
85
80
|
duration = output[:duration]
|
86
|
-
puts "#{duration
|
81
|
+
puts "#{duration.minutes}:#{duration.seconds.to_s.rjust(2, '0')} of audio written in #{Time.now - start_time} seconds."
|
87
82
|
rescue => error
|
88
83
|
print_error(error, input_file_name)
|
89
84
|
exit(false)
|
data/ext/mkrf_conf.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Based on: http://www.programmersparadox.com/2012/05/21/gemspec-loading-dependent-gems-based-on-the-users-system/
|
2
|
+
|
3
|
+
# This file needs to be named mkrf_conf.rb
|
4
|
+
# so that rubygems will recognize it as a ruby extension
|
5
|
+
# file and not think it is a C extension file
|
6
|
+
|
7
|
+
require 'rubygems/dependency_installer.rb'
|
8
|
+
|
9
|
+
# Load up the rubygem's dependency installer to
|
10
|
+
# installer the gems we want based on the version
|
11
|
+
# of Ruby the user has installed
|
12
|
+
installer = Gem::DependencyInstaller.new
|
13
|
+
begin
|
14
|
+
if RUBY_VERSION >= "2"
|
15
|
+
installer.install "syck"
|
16
|
+
end
|
17
|
+
|
18
|
+
rescue
|
19
|
+
# Exit with a non-zero value to let rubygems something went wrong
|
20
|
+
exit(1)
|
21
|
+
end
|
22
|
+
|
23
|
+
# If this was C, rubygems would attempt to run make
|
24
|
+
# Since this is Ruby, rubygems will attempt to run rake
|
25
|
+
# If it doesn't find and successfully run a rakefile, it errors out
|
26
|
+
f = File.open(File.join(File.dirname(__FILE__), "Rakefile"), "w")
|
27
|
+
f.write("task :default\n")
|
28
|
+
f.close
|
data/lib/beats.rb
CHANGED
@@ -1,77 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
output_file_name = File.basename(input_file_name, File.extname(input_file_name)) + ".wav"
|
14
|
-
end
|
15
|
-
@output_file_name = output_file_name
|
16
|
-
|
17
|
-
@options = options
|
18
|
-
end
|
19
|
-
|
20
|
-
def run
|
21
|
-
base_path = @options[:base_path] || File.dirname(@input_file_name)
|
22
|
-
song, kit = SongParser.new().parse(base_path, File.read(@input_file_name))
|
23
|
-
|
24
|
-
song = normalize_for_pattern_option(song)
|
25
|
-
songs_to_generate = normalize_for_split_option(song)
|
26
|
-
|
27
|
-
duration = nil
|
28
|
-
song_optimizer = SongOptimizer.new()
|
29
|
-
songs_to_generate.each do |output_file_name, song|
|
30
|
-
song = song_optimizer.optimize(song, OPTIMIZED_PATTERN_LENGTH)
|
31
|
-
duration = AudioEngine.new(song, kit).write_to_file(output_file_name)
|
32
|
-
end
|
33
|
-
|
34
|
-
return {:duration => duration}
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
# If the -p option is used, transform the song into one whose flow consists of
|
40
|
-
# playing that single pattern once.
|
41
|
-
def normalize_for_pattern_option(song)
|
42
|
-
unless @options[:pattern] == nil
|
43
|
-
pattern_name = @options[:pattern].downcase.to_sym
|
44
|
-
|
45
|
-
unless song.patterns.has_key?(pattern_name)
|
46
|
-
raise StandardError, "The song does not include a pattern called #{pattern_name}"
|
47
|
-
end
|
48
|
-
|
49
|
-
song.flow = [pattern_name]
|
50
|
-
song.remove_unused_patterns()
|
51
|
-
end
|
52
|
-
|
53
|
-
return song
|
54
|
-
end
|
55
|
-
|
56
|
-
# Returns a hash of file name => song object for each song that should go through the audio engine
|
57
|
-
def normalize_for_split_option(song)
|
58
|
-
songs_to_generate = {}
|
59
|
-
|
60
|
-
if @options[:split]
|
61
|
-
split_songs = song.split()
|
62
|
-
split_songs.each do |track_name, split_song|
|
63
|
-
# TODO: Move building the output file name into its own method?
|
64
|
-
extension = File.extname(@output_file_name)
|
65
|
-
file_name = File.dirname(@output_file_name) + "/" +
|
66
|
-
File.basename(@output_file_name, extension) + "-" + File.basename(track_name, extension) +
|
67
|
-
extension
|
68
|
-
|
69
|
-
songs_to_generate[file_name] = split_song
|
70
|
-
end
|
71
|
-
else
|
72
|
-
songs_to_generate[@output_file_name] = song
|
73
|
-
end
|
74
|
-
|
75
|
-
return songs_to_generate
|
76
|
-
end
|
1
|
+
require 'beats/audioengine'
|
2
|
+
require 'beats/audioutils'
|
3
|
+
require 'beats/beatsrunner'
|
4
|
+
require 'beats/kit'
|
5
|
+
require 'beats/pattern'
|
6
|
+
require 'beats/song'
|
7
|
+
require 'beats/songparser'
|
8
|
+
require 'beats/songoptimizer'
|
9
|
+
require 'beats/track'
|
10
|
+
|
11
|
+
module Beats
|
12
|
+
VERSION = "1.2.5"
|
77
13
|
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module Beats
|
2
|
+
# This class actually generates the output audio data that is saved to disk.
|
3
|
+
#
|
4
|
+
# To produce audio data, it needs two things: a Song and a Kit. The Song tells
|
5
|
+
# it which sounds to trigger and when, while the Kit provides the sample data
|
6
|
+
# for each of these sounds.
|
7
|
+
#
|
8
|
+
# Example usage, assuming song and kit are already defined:
|
9
|
+
#
|
10
|
+
# engine = AudioEngine.new(song, kit)
|
11
|
+
# engine.write_to_file("my_song.wav")
|
12
|
+
#
|
13
|
+
class AudioEngine
|
14
|
+
SAMPLE_RATE = 44100
|
15
|
+
PACK_CODE = "s*" # All output sample data is assumed to be 16-bit
|
16
|
+
|
17
|
+
def initialize(song, kit)
|
18
|
+
@song = song
|
19
|
+
@kit = kit
|
20
|
+
|
21
|
+
@step_sample_length = AudioUtils.step_sample_length(SAMPLE_RATE, @song.tempo)
|
22
|
+
@composited_pattern_cache = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def write_to_file(output_file_name)
|
26
|
+
packed_pattern_cache = {}
|
27
|
+
num_tracks_in_song = @song.total_tracks
|
28
|
+
|
29
|
+
# Open output wave file and prepare it for writing sample data.
|
30
|
+
format = WaveFile::Format.new(@kit.num_channels, @kit.bits_per_sample, SAMPLE_RATE)
|
31
|
+
writer = WaveFile::CachingWriter.new(output_file_name, format)
|
32
|
+
|
33
|
+
# Generate each pattern's sample data, or pull it from cache, and append it to the wave file.
|
34
|
+
incoming_overflow = {}
|
35
|
+
@song.flow.each do |pattern_name|
|
36
|
+
key = [pattern_name, incoming_overflow.hash]
|
37
|
+
unless packed_pattern_cache.member?(key)
|
38
|
+
sample_data = generate_pattern_sample_data(@song.patterns[pattern_name], incoming_overflow)
|
39
|
+
|
40
|
+
packed_pattern_cache[key] = { :primary => WaveFile::Buffer.new(sample_data[:primary], format),
|
41
|
+
:overflow => WaveFile::Buffer.new(sample_data[:overflow], format) }
|
42
|
+
end
|
43
|
+
|
44
|
+
writer.write(packed_pattern_cache[key][:primary])
|
45
|
+
incoming_overflow = packed_pattern_cache[key][:overflow].samples
|
46
|
+
end
|
47
|
+
|
48
|
+
# Write any remaining overflow from the final pattern
|
49
|
+
final_overflow_composite = AudioUtils.composite(incoming_overflow.values, format.channels)
|
50
|
+
final_overflow_composite = AudioUtils.scale(final_overflow_composite, format.channels, num_tracks_in_song)
|
51
|
+
writer.write(WaveFile::Buffer.new(final_overflow_composite, format))
|
52
|
+
|
53
|
+
writer.close()
|
54
|
+
|
55
|
+
writer.total_duration
|
56
|
+
end
|
57
|
+
|
58
|
+
attr_reader :step_sample_length
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Generates the sample data for a single track, using the specified sound's sample data.
|
63
|
+
def generate_track_sample_data(track, sound)
|
64
|
+
beats = track.beats
|
65
|
+
if beats == [0]
|
66
|
+
return {:primary => [], :overflow => []} # Is this really what should happen? Why throw away overflow?
|
67
|
+
end
|
68
|
+
|
69
|
+
fill_value = (@kit.num_channels == 1) ? 0 : [0, 0]
|
70
|
+
primary_sample_data = [].fill(fill_value, 0, AudioUtils.step_start_sample(track.step_count, @step_sample_length))
|
71
|
+
|
72
|
+
step_index = beats[0]
|
73
|
+
beat_sample_length = 0
|
74
|
+
beats[1...(beats.length)].each do |beat_step_length|
|
75
|
+
start_sample = AudioUtils.step_start_sample(step_index, @step_sample_length)
|
76
|
+
end_sample = [(start_sample + sound.length), primary_sample_data.length].min
|
77
|
+
beat_sample_length = end_sample - start_sample
|
78
|
+
|
79
|
+
primary_sample_data[start_sample...end_sample] = sound[0...beat_sample_length]
|
80
|
+
|
81
|
+
step_index += beat_step_length
|
82
|
+
end
|
83
|
+
|
84
|
+
overflow_sample_data = (sound == [] || beats.length == 1) ? [] : sound[beat_sample_length...(sound.length)]
|
85
|
+
|
86
|
+
{:primary => primary_sample_data, :overflow => overflow_sample_data}
|
87
|
+
end
|
88
|
+
|
89
|
+
# Composites the sample data for each of the pattern's tracks, and returns the overflow sample data
|
90
|
+
# from tracks whose last sound trigger extends past the end of the pattern. This overflow can be
|
91
|
+
# used by the next pattern to avoid sounds cutting off when the pattern changes.
|
92
|
+
def generate_pattern_sample_data(pattern, incoming_overflow)
|
93
|
+
# Unless cached, composite each track's sample data.
|
94
|
+
if @composited_pattern_cache[pattern].nil?
|
95
|
+
primary_sample_data, overflow_sample_data = composite_pattern_tracks(pattern)
|
96
|
+
@composited_pattern_cache[pattern] = {:primary => primary_sample_data.dup, :overflow => overflow_sample_data.dup}
|
97
|
+
else
|
98
|
+
primary_sample_data = @composited_pattern_cache[pattern][:primary].dup
|
99
|
+
overflow_sample_data = @composited_pattern_cache[pattern][:overflow].dup
|
100
|
+
end
|
101
|
+
|
102
|
+
# Composite overflow from the previous pattern onto this pattern, to prevent sounds from cutting off.
|
103
|
+
primary_sample_data, overflow_sample_data = handle_incoming_overflow(pattern,
|
104
|
+
incoming_overflow,
|
105
|
+
primary_sample_data,
|
106
|
+
overflow_sample_data)
|
107
|
+
primary_sample_data = AudioUtils.scale(primary_sample_data, @kit.num_channels, @song.total_tracks)
|
108
|
+
|
109
|
+
{:primary => primary_sample_data, :overflow => overflow_sample_data}
|
110
|
+
end
|
111
|
+
|
112
|
+
def composite_pattern_tracks(pattern)
|
113
|
+
overflow_sample_data = {}
|
114
|
+
|
115
|
+
raw_track_sample_arrays = []
|
116
|
+
pattern.tracks.each do |track_name, track|
|
117
|
+
temp = generate_track_sample_data(track, @kit.get_sample_data(track.name))
|
118
|
+
raw_track_sample_arrays << temp[:primary]
|
119
|
+
overflow_sample_data[track_name] = temp[:overflow]
|
120
|
+
end
|
121
|
+
|
122
|
+
primary_sample_data = AudioUtils.composite(raw_track_sample_arrays, @kit.num_channels)
|
123
|
+
return primary_sample_data, overflow_sample_data
|
124
|
+
end
|
125
|
+
|
126
|
+
# Applies sound overflow (i.e. long sounds such as cymbal crash which extend past the last step)
|
127
|
+
# from the previous pattern in the flow to the current pattern. This prevents sounds from being
|
128
|
+
# cut off when the pattern changes.
|
129
|
+
#
|
130
|
+
# It would probably be shorter and conceptually simpler to deal with incoming overflow in
|
131
|
+
# generate_track_sample_data() instead of this method. (In fact, this method would go away).
|
132
|
+
# However, doing it this way allows for caching composited pattern sample data, and
|
133
|
+
# applying incoming overflow to the composite. This allows each pattern to only be composited once,
|
134
|
+
# regardless of the incoming overflow that each performance of it receives. If incoming overflow
|
135
|
+
# was handled at the Track level we couldn't do that.
|
136
|
+
def handle_incoming_overflow(pattern, incoming_overflow, primary_sample_data, overflow_sample_data)
|
137
|
+
pattern_track_names = pattern.tracks.keys
|
138
|
+
sample_arrays = [primary_sample_data]
|
139
|
+
|
140
|
+
incoming_overflow.each do |incoming_track_name, incoming_sample_data|
|
141
|
+
end_sample = incoming_sample_data.length
|
142
|
+
|
143
|
+
if pattern_track_names.member?(incoming_track_name)
|
144
|
+
track = pattern.tracks[incoming_track_name]
|
145
|
+
|
146
|
+
if track.beats.length > 1
|
147
|
+
intro_length = (pattern.tracks[incoming_track_name].beats[0] * step_sample_length).floor
|
148
|
+
end_sample = [end_sample, intro_length].min
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
if end_sample > primary_sample_data.length
|
153
|
+
end_sample = primary_sample_data.length
|
154
|
+
overflow_sample_data[incoming_track_name] = incoming_sample_data[(primary_sample_data.length)...(incoming_sample_data.length)]
|
155
|
+
end
|
156
|
+
|
157
|
+
sample_arrays << incoming_sample_data[0...end_sample]
|
158
|
+
end
|
159
|
+
|
160
|
+
return AudioUtils.composite(sample_arrays, @kit.num_channels), overflow_sample_data
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Beats
|
2
|
+
# This class contains some utility methods for working with sample data.
|
3
|
+
module AudioUtils
|
4
|
+
|
5
|
+
# Combines multiple sample arrays into one, by adding them together.
|
6
|
+
# When the sample arrays are different lengths, the output array will be the length
|
7
|
+
# of the longest input array.
|
8
|
+
# WARNING: Incoming arrays can be modified.
|
9
|
+
def self.composite(sample_arrays, num_channels)
|
10
|
+
if sample_arrays == []
|
11
|
+
return []
|
12
|
+
end
|
13
|
+
|
14
|
+
# Sort from longest to shortest
|
15
|
+
sample_arrays = sample_arrays.sort {|x, y| y.length <=> x.length}
|
16
|
+
|
17
|
+
composited_output = sample_arrays.slice!(0)
|
18
|
+
sample_arrays.each do |sample_array|
|
19
|
+
unless sample_array == []
|
20
|
+
if num_channels == 1
|
21
|
+
sample_array.length.times {|i| composited_output[i] += sample_array[i] }
|
22
|
+
elsif num_channels == 2
|
23
|
+
sample_array.length.times do |i|
|
24
|
+
composited_output[i] = [composited_output[i][0] + sample_array[i][0],
|
25
|
+
composited_output[i][1] + sample_array[i][1]]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
composited_output
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# Scales the amplitude of the incoming sample array by *scale* amount. Can be used in conjunction
|
36
|
+
# with composite() to make sure composited sample arrays don't have an amplitude greater than 1.0.
|
37
|
+
def self.scale(sample_array, num_channels, scale)
|
38
|
+
if sample_array == []
|
39
|
+
return sample_array
|
40
|
+
end
|
41
|
+
|
42
|
+
if scale > 1
|
43
|
+
if num_channels == 1
|
44
|
+
sample_array = sample_array.map {|sample| sample / scale }
|
45
|
+
elsif num_channels == 2
|
46
|
+
sample_array = sample_array.map {|sample| [sample[0] / scale, sample[1] / scale]}
|
47
|
+
else
|
48
|
+
raise StandardError, "Invalid sample data array in AudioUtils.normalize()"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
sample_array
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# Returns the number of samples that each step (i.e. a 'X' or a '.') lasts at a given sample
|
57
|
+
# rate and tempo. The sample length can be a non-integer value. Although there's no such
|
58
|
+
# thing as a partial sample, this is required to prevent small timing errors from creeping in.
|
59
|
+
# If they accumulate, they can cause rhythms to drift out of time.
|
60
|
+
def self.step_sample_length(samples_per_second, tempo)
|
61
|
+
samples_per_minute = samples_per_second * 60.0
|
62
|
+
samples_per_quarter_note = samples_per_minute / tempo
|
63
|
+
|
64
|
+
# Each step is equivalent to a 16th note
|
65
|
+
samples_per_quarter_note / 4.0
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
# Returns the sample index that a given step (offset from 0) starts on.
|
70
|
+
def self.step_start_sample(step_index, step_sample_length)
|
71
|
+
(step_index * step_sample_length).floor
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|