beats 1.2.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/README.markdown +28 -10
- data/bin/beats +9 -7
- data/lib/audioengine.rb +172 -0
- data/lib/audioutils.rb +73 -0
- data/lib/beats.rb +14 -15
- data/lib/beatswavefile.rb +17 -37
- data/lib/kit.rb +148 -71
- data/lib/pattern.rb +20 -117
- data/lib/patternexpander.rb +111 -0
- data/lib/song.rb +78 -132
- data/lib/songoptimizer.rb +29 -33
- data/lib/songparser.rb +70 -45
- data/lib/track.rb +11 -82
- data/test/audioengine_test.rb +261 -0
- data/test/audioutils_test.rb +45 -0
- data/test/fixtures/expected_output/example_split_mono_16-hh_closed.wav +0 -0
- data/test/{examples/split-agogo_high.wav → fixtures/expected_output/example_split_mono_16-hh_closed2.wav} +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-hh_closed.wav +0 -0
- data/test/{examples/split-tom4.wav → fixtures/expected_output/example_split_mono_8-hh_closed2.wav} +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-hh_closed2.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-hh_closed2.wav +0 -0
- data/test/fixtures/invalid/{bad_structure.txt → bad_flow.txt} +2 -2
- data/test/fixtures/invalid/bad_repeat_count.txt +1 -1
- data/test/fixtures/invalid/bad_rhythm.txt +1 -1
- data/test/fixtures/invalid/bad_tempo.txt +1 -1
- data/test/fixtures/invalid/{no_structure.txt → no_flow.txt} +1 -1
- data/test/fixtures/invalid/pattern_with_no_tracks.txt +1 -1
- data/test/fixtures/invalid/sound_in_kit_not_found.txt +1 -1
- data/test/fixtures/invalid/sound_in_kit_wrong_format.txt +10 -0
- data/test/fixtures/invalid/sound_in_track_not_found.txt +1 -1
- data/test/fixtures/invalid/sound_in_track_wrong_format.txt +8 -0
- data/test/fixtures/invalid/template.txt +1 -1
- data/test/fixtures/valid/example_mono_16.txt +5 -3
- data/test/fixtures/valid/example_mono_8.txt +5 -3
- data/test/fixtures/valid/example_no_kit.txt +1 -1
- data/test/fixtures/valid/example_stereo_16.txt +7 -4
- data/test/fixtures/valid/example_stereo_8.txt +5 -3
- data/test/fixtures/valid/example_with_empty_track.txt +1 -1
- data/test/fixtures/valid/example_with_kit.txt +1 -1
- data/test/fixtures/valid/multiple_tracks_same_sound.txt +33 -0
- data/test/fixtures/valid/no_tempo.txt +1 -1
- data/test/fixtures/valid/optimize_pattern_collision.txt +28 -0
- data/test/fixtures/valid/pattern_with_overflow.txt +1 -1
- data/test/fixtures/valid/repeats_not_specified.txt +2 -2
- data/test/fixtures/valid/with_structure.txt +10 -0
- data/test/fixtures/yaml/song_yaml.txt +5 -5
- data/test/includes.rb +4 -2
- data/test/integration.rb +3 -3
- data/test/kit_test.rb +136 -109
- data/test/pattern_test.rb +31 -131
- data/test/patternexpander_test.rb +142 -0
- data/test/song_test.rb +104 -102
- data/test/songoptimizer_test.rb +52 -38
- data/test/songparser_test.rb +79 -46
- data/test/sounds/composite_snare_mono_8_tom3_mono_16_mono_16.wav +0 -0
- data/test/sounds/composite_snare_mono_8_tom3_mono_8_mono_16.wav +0 -0
- data/test/sounds/composite_snare_stereo_16_tom3_mono_16_stereo_16.wav +0 -0
- data/test/sounds/composite_snare_stereo_8_tom3_mono_16_stereo_16.wav +0 -0
- data/test/track_test.rb +30 -185
- metadata +56 -24
- data/lib/songsplitter.rb +0 -38
- data/test/examples/combined.wav +0 -0
- data/test/examples/split-bass.wav +0 -0
- data/test/examples/split-hh_closed.wav +0 -0
- data/test/examples/split-snare.wav +0 -0
- data/test/examples/split-tom2.wav +0 -0
data/LICENSE
CHANGED
data/README.markdown
CHANGED
@@ -12,9 +12,9 @@ BEATS is a command-line drum machine written in pure Ruby. Feed it a song notate
|
|
12
12
|
- Chorus: x4
|
13
13
|
Kit:
|
14
14
|
- bass: sounds/bass.wav
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
- snare: sounds/snare.wav
|
16
|
+
- hh_closed: sounds/hh_closed.wav
|
17
|
+
- agogo: sounds/agogo_high.wav
|
18
18
|
|
19
19
|
Verse:
|
20
20
|
- bass: X...X...X...X...
|
@@ -26,23 +26,27 @@ BEATS is a command-line drum machine written in pure Ruby. Feed it a song notate
|
|
26
26
|
- bass: X...X...X...X...
|
27
27
|
- snare: ....X.......X...
|
28
28
|
- hh_closed: X.XXX.XXX.XX..X.
|
29
|
-
|
30
|
-
|
29
|
+
- sounds/tom4.wav: ...........X....
|
30
|
+
- sounds/tom2.wav: ..............X.
|
31
|
+
|
32
|
+
And [here's what it sounds like](http://beatsdrummachine.com/beat.mp3) after getting the BEATS treatment. What a glorious groove!
|
31
33
|
|
32
|
-
And [here is what it sounds like](http://beatsdrummachine.com/beat.mp3) after getting the BEATS treatment. What a glorious groove!
|
33
34
|
|
34
35
|
Current Status
|
35
36
|
--------------
|
36
37
|
|
37
|
-
The latest stable version of BEATS is 1.2.
|
38
|
+
The latest stable version of BEATS is 1.2.1, released on March 6, 2011. This is a minor release which includes the following improvments:
|
38
39
|
|
39
|
-
|
40
|
+
* You can use the | character to represent bar lines in a track rhythm. This is optional, but often makes longer rhythms easier to read.
|
41
|
+
* The "Structure" section of the song header is now called "Flow". (You can still use "Structure" for now, but you'll get a warning).
|
42
|
+
* A pattern can contain multiple tracks that use the same sound. Previously, BEATS would pick one of those tracks as the 'winner', and the other tracks wouldn't be played.
|
43
|
+
* Bug fix: A better error message is displayed if a sound file is in an unsupported format (such as MP3), or is not even a sound file.
|
40
44
|
|
41
45
|
|
42
46
|
Installation
|
43
47
|
------------
|
44
48
|
|
45
|
-
To install the latest stable version (1.2.
|
49
|
+
To install the latest stable version (1.2.1) from [rubygems.org](http://rubygems.org/gems/beats), run the following from the command line:
|
46
50
|
|
47
51
|
sudo gem install beats
|
48
52
|
|
@@ -54,4 +58,18 @@ BEATS is not very useful unless you have some sounds to use with it. You can dow
|
|
54
58
|
Usage
|
55
59
|
-----
|
56
60
|
|
57
|
-
BEATS runs from the command-line. Run `beats -h` to see the available options. For more detailed instructions, visit [
|
61
|
+
BEATS runs from the command-line. Run `beats -h` to see the available options. For more detailed instructions, visit [https://github.com/jstrait/beats/wiki/Usage](https://github.com/jstrait/beats/wiki/Usage) on the [BEATS Wiki](https://github.com/jstrait/beats/wiki).
|
62
|
+
|
63
|
+
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.
|
64
|
+
|
65
|
+
|
66
|
+
Found a Bug? Have a Suggestion? Want to Contribute?
|
67
|
+
---------------------------------------------------
|
68
|
+
|
69
|
+
Contact me (Joel Strait) by sending a GitHub message.
|
70
|
+
|
71
|
+
|
72
|
+
License
|
73
|
+
-------
|
74
|
+
BEATS is released under the MIT license.
|
75
|
+
|
data/bin/beats
CHANGED
@@ -5,16 +5,18 @@ start_time = Time.now
|
|
5
5
|
$:.unshift File.dirname(__FILE__) + "/.."
|
6
6
|
require "optparse"
|
7
7
|
require "yaml"
|
8
|
+
require "lib/wavefile"
|
9
|
+
require "lib/beatswavefile"
|
10
|
+
require "lib/audioengine"
|
11
|
+
require "lib/audioutils"
|
8
12
|
require "lib/beats"
|
9
|
-
require "lib/song"
|
10
|
-
require "lib/songparser"
|
11
|
-
require "lib/songoptimizer"
|
12
|
-
require "lib/songsplitter"
|
13
13
|
require "lib/kit"
|
14
14
|
require "lib/pattern"
|
15
|
+
require "lib/patternexpander"
|
16
|
+
require "lib/song"
|
17
|
+
require "lib/songoptimizer"
|
18
|
+
require "lib/songparser"
|
15
19
|
require "lib/track"
|
16
|
-
require "lib/wavefile"
|
17
|
-
require "lib/beatswavefile"
|
18
20
|
|
19
21
|
def parse_options
|
20
22
|
options = {:split => false, :pattern => nil}
|
@@ -67,4 +69,4 @@ rescue StandardError => detail
|
|
67
69
|
puts "An error occured while generating sound for '#{input_file_name}':\n"
|
68
70
|
puts " #{detail}\n"
|
69
71
|
puts "\n"
|
70
|
-
end
|
72
|
+
end
|
data/lib/audioengine.rb
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
# This class actually generates the output sound data for the performance.
|
2
|
+
# Applies a Kit to a Song (which contains sub Patterns and Tracks) to
|
3
|
+
# produce output sample data.
|
4
|
+
class AudioEngine
|
5
|
+
SAMPLE_RATE = 44100
|
6
|
+
PACK_CODE = "s*" # All output sample data is assumed to be 16-bit
|
7
|
+
|
8
|
+
def initialize(song, kit)
|
9
|
+
@song = song
|
10
|
+
@kit = kit
|
11
|
+
|
12
|
+
@step_sample_length = AudioUtils.step_sample_length(SAMPLE_RATE, @song.tempo)
|
13
|
+
@composited_pattern_cache = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def write_to_file(output_file_name)
|
17
|
+
packed_pattern_cache = {}
|
18
|
+
num_tracks_in_song = @song.total_tracks
|
19
|
+
samples_written = 0
|
20
|
+
|
21
|
+
# Open output wave file and preparing it for writing sample data.
|
22
|
+
wave_file = BeatsWaveFile.new(@kit.num_channels, SAMPLE_RATE, @kit.bits_per_sample)
|
23
|
+
file = wave_file.open_for_appending(output_file_name)
|
24
|
+
|
25
|
+
# Generate each pattern's sample data, or pull it from cache, and append it to the wave file.
|
26
|
+
incoming_overflow = {}
|
27
|
+
@song.flow.each do |pattern_name|
|
28
|
+
key = [pattern_name, incoming_overflow.hash]
|
29
|
+
unless packed_pattern_cache.member?(key)
|
30
|
+
sample_data = generate_pattern_sample_data(@song.patterns[pattern_name], incoming_overflow)
|
31
|
+
|
32
|
+
if @kit.num_channels == 1
|
33
|
+
# Don't flatten the sample data Array, since it is already flattened. That would be a waste of time, yo.
|
34
|
+
packed_pattern_cache[key] = {:primary => sample_data[:primary].pack(PACK_CODE),
|
35
|
+
:overflow => sample_data[:overflow],
|
36
|
+
:primary_length => sample_data[:primary].length}
|
37
|
+
else
|
38
|
+
packed_pattern_cache[key] = {:primary => sample_data[:primary].flatten.pack(PACK_CODE),
|
39
|
+
:overflow => sample_data[:overflow],
|
40
|
+
:primary_length => sample_data[:primary].length}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
file.syswrite(packed_pattern_cache[key][:primary])
|
45
|
+
incoming_overflow = packed_pattern_cache[key][:overflow]
|
46
|
+
samples_written += packed_pattern_cache[key][:primary_length]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Write any remaining overflow from the final pattern
|
50
|
+
final_overflow_composite = AudioUtils.composite(incoming_overflow.values, @kit.num_channels)
|
51
|
+
final_overflow_composite = AudioUtils.scale(final_overflow_composite, @kit.num_channels, num_tracks_in_song)
|
52
|
+
if @kit.num_channels == 1
|
53
|
+
file.syswrite(final_overflow_composite.pack(PACK_CODE))
|
54
|
+
else
|
55
|
+
file.syswrite(final_overflow_composite.flatten.pack(PACK_CODE))
|
56
|
+
end
|
57
|
+
samples_written += final_overflow_composite.length
|
58
|
+
|
59
|
+
# Now that we know how many samples have been written, go back and re-write the correct header.
|
60
|
+
file.sysseek(0)
|
61
|
+
wave_file.write_header(file, samples_written)
|
62
|
+
|
63
|
+
file.close()
|
64
|
+
|
65
|
+
return wave_file.calculate_duration(SAMPLE_RATE, samples_written)
|
66
|
+
end
|
67
|
+
|
68
|
+
attr_reader :step_sample_length
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# Generates the sample data for a single track, using the specified sound's sample data.
|
73
|
+
def generate_track_sample_data(track, sound)
|
74
|
+
beats = track.beats
|
75
|
+
if beats == [0]
|
76
|
+
return {:primary => [], :overflow => []} # Is this really what should happen? Why throw away overflow?
|
77
|
+
end
|
78
|
+
|
79
|
+
fill_value = (@kit.num_channels == 1) ? 0 : [0, 0]
|
80
|
+
primary_sample_data = [].fill(fill_value, 0, AudioUtils.step_start_sample(track.step_count, @step_sample_length))
|
81
|
+
|
82
|
+
step_index = beats[0]
|
83
|
+
beat_sample_length = 0
|
84
|
+
beats[1...(beats.length)].each do |beat_step_length|
|
85
|
+
start_sample = AudioUtils.step_start_sample(step_index, @step_sample_length)
|
86
|
+
end_sample = [(start_sample + sound.length), primary_sample_data.length].min
|
87
|
+
beat_sample_length = end_sample - start_sample
|
88
|
+
|
89
|
+
primary_sample_data[start_sample...end_sample] = sound[0...beat_sample_length]
|
90
|
+
|
91
|
+
step_index += beat_step_length
|
92
|
+
end
|
93
|
+
|
94
|
+
overflow_sample_data = (sound == [] || beats.length == 1) ? [] : sound[beat_sample_length...(sound.length)]
|
95
|
+
|
96
|
+
return {:primary => primary_sample_data, :overflow => overflow_sample_data}
|
97
|
+
end
|
98
|
+
|
99
|
+
# Composites the sample data for each of the pattern's tracks, and returns the overflow sample data
|
100
|
+
# from tracks whose last sound trigger extends past the end of the pattern. This overflow can be
|
101
|
+
# used by the next pattern to avoid sounds cutting off when the pattern changes.
|
102
|
+
def generate_pattern_sample_data(pattern, incoming_overflow)
|
103
|
+
# Unless cached, composite each track's sample data.
|
104
|
+
if @composited_pattern_cache[pattern] == nil
|
105
|
+
primary_sample_data, overflow_sample_data = composite_pattern_tracks(pattern)
|
106
|
+
@composited_pattern_cache[pattern] = {:primary => primary_sample_data.dup, :overflow => overflow_sample_data.dup}
|
107
|
+
else
|
108
|
+
primary_sample_data = @composited_pattern_cache[pattern][:primary].dup
|
109
|
+
overflow_sample_data = @composited_pattern_cache[pattern][:overflow].dup
|
110
|
+
end
|
111
|
+
|
112
|
+
# Composite overflow from the previous pattern onto this pattern, to prevent sounds from cutting off.
|
113
|
+
primary_sample_data, overflow_sample_data = handle_incoming_overflow(pattern,
|
114
|
+
incoming_overflow,
|
115
|
+
primary_sample_data,
|
116
|
+
overflow_sample_data)
|
117
|
+
primary_sample_data = AudioUtils.scale(primary_sample_data, @kit.num_channels, @song.total_tracks)
|
118
|
+
|
119
|
+
return {:primary => primary_sample_data, :overflow => overflow_sample_data}
|
120
|
+
end
|
121
|
+
|
122
|
+
def composite_pattern_tracks(pattern)
|
123
|
+
overflow_sample_data = {}
|
124
|
+
|
125
|
+
raw_track_sample_arrays = []
|
126
|
+
pattern.tracks.each do |track_name, track|
|
127
|
+
temp = generate_track_sample_data(track, @kit.get_sample_data(track.name))
|
128
|
+
raw_track_sample_arrays << temp[:primary]
|
129
|
+
overflow_sample_data[track_name] = temp[:overflow]
|
130
|
+
end
|
131
|
+
|
132
|
+
primary_sample_data = AudioUtils.composite(raw_track_sample_arrays, @kit.num_channels)
|
133
|
+
return primary_sample_data, overflow_sample_data
|
134
|
+
end
|
135
|
+
|
136
|
+
# Applies sound overflow (i.e. long sounds such as cymbal crash which extend past the last step)
|
137
|
+
# from the previous pattern in the flow to the current pattern. This prevents sounds from being
|
138
|
+
# cut off when the pattern changes.
|
139
|
+
#
|
140
|
+
# It would probably be shorter and conceptually simpler to deal with incoming overflow in
|
141
|
+
# generate_track_sample_data() instead of this method. (In fact, this method would go away).
|
142
|
+
# However, doing it this way allows for caching composited pattern sample data, and
|
143
|
+
# applying incoming overflow to the composite. This allows each pattern to only be composited once,
|
144
|
+
# regardless of the incoming overflow that each performance of it receives. If incoming overflow
|
145
|
+
# was handled at the Track level we couldn't do that.
|
146
|
+
def handle_incoming_overflow(pattern, incoming_overflow, primary_sample_data, overflow_sample_data)
|
147
|
+
pattern_track_names = pattern.tracks.keys
|
148
|
+
sample_arrays = [primary_sample_data]
|
149
|
+
|
150
|
+
incoming_overflow.each do |incoming_track_name, incoming_sample_data|
|
151
|
+
end_sample = incoming_sample_data.length
|
152
|
+
|
153
|
+
if pattern_track_names.member?(incoming_track_name)
|
154
|
+
track = pattern.tracks[incoming_track_name]
|
155
|
+
|
156
|
+
if track.beats.length > 1
|
157
|
+
intro_length = (pattern.tracks[incoming_track_name].beats[0] * step_sample_length).floor
|
158
|
+
end_sample = [end_sample, intro_length].min
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
if end_sample > primary_sample_data.length
|
163
|
+
end_sample = primary_sample_data.length
|
164
|
+
overflow_sample_data[incoming_track_name] = incoming_sample_data[(primary_sample_data.length)...(incoming_sample_data.length)]
|
165
|
+
end
|
166
|
+
|
167
|
+
sample_arrays << incoming_sample_data[0...end_sample]
|
168
|
+
end
|
169
|
+
|
170
|
+
return AudioUtils.composite(sample_arrays, @kit.num_channels), overflow_sample_data
|
171
|
+
end
|
172
|
+
end
|
data/lib/audioutils.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# This class contains some utility methods for working with sample data.
|
2
|
+
class AudioUtils
|
3
|
+
|
4
|
+
# Combines multiple sample arrays into one, by adding them together.
|
5
|
+
# When the sample arrays are different lengths, the output array will be the length
|
6
|
+
# of the longest input array.
|
7
|
+
# WARNING: Incoming arrays can be modified.
|
8
|
+
def self.composite(sample_arrays, num_channels)
|
9
|
+
if sample_arrays == []
|
10
|
+
return []
|
11
|
+
end
|
12
|
+
|
13
|
+
# Sort from longest to shortest
|
14
|
+
sample_arrays = sample_arrays.sort {|x, y| y.length <=> x.length}
|
15
|
+
|
16
|
+
composited_output = sample_arrays.slice!(0)
|
17
|
+
sample_arrays.each do |sample_array|
|
18
|
+
unless sample_array == []
|
19
|
+
if num_channels == 1
|
20
|
+
sample_array.length.times {|i| composited_output[i] += sample_array[i] }
|
21
|
+
elsif num_channels == 2
|
22
|
+
sample_array.length.times do |i|
|
23
|
+
composited_output[i] = [composited_output[i][0] + sample_array[i][0],
|
24
|
+
composited_output[i][1] + sample_array[i][1]]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
return composited_output
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
# Scales the amplitude of the incoming sample array by *scale* amount. Can be used in conjunction
|
35
|
+
# with composite() to make sure composited sample arrays don't have an amplitude greater than 1.0.
|
36
|
+
def self.scale(sample_array, num_channels, scale)
|
37
|
+
if sample_array == []
|
38
|
+
return sample_array
|
39
|
+
end
|
40
|
+
|
41
|
+
if scale > 1
|
42
|
+
if num_channels == 1
|
43
|
+
sample_array = sample_array.map {|sample| sample / scale }
|
44
|
+
elsif num_channels == 2
|
45
|
+
sample_array = sample_array.map {|sample| [sample[0] / scale, sample[1] / scale]}
|
46
|
+
else
|
47
|
+
raise StandardError, "Invalid sample data array in AudioUtils.normalize()"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
return sample_array
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
# Returns the number of samples that each step (i.e. a 'X' or a '.') lasts at a given sample
|
56
|
+
# rate and tempo. The sample length can be a non-integer value. Although there's no such
|
57
|
+
# thing as a partial sample, this is required to prevent small timing errors from creeping in.
|
58
|
+
# If they accumulate, they can cause rhythms to drift out of time.
|
59
|
+
def self.step_sample_length(samples_per_second, tempo)
|
60
|
+
samples_per_minute = samples_per_second * 60.0
|
61
|
+
samples_per_quarter_note = samples_per_minute / tempo
|
62
|
+
|
63
|
+
# Each step is equivalent to a 16th note
|
64
|
+
return samples_per_quarter_note / 4.0
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# Returns the sample index that a given step (offset from 0) starts on.
|
69
|
+
def self.step_start_sample(step_index, step_sample_length)
|
70
|
+
return (step_index * step_sample_length).floor
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
data/lib/beats.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
class Beats
|
2
|
-
BEATS_VERSION = "1.2.
|
3
|
-
|
2
|
+
BEATS_VERSION = "1.2.1"
|
3
|
+
|
4
|
+
# Each pattern in the song will be split up into sub patterns that have at most this many steps.
|
5
|
+
# In general, audio for several shorter patterns can be generated more quickly than for one long
|
6
|
+
# pattern, and can also be cached more effectively.
|
4
7
|
OPTIMIZED_PATTERN_LENGTH = 4
|
5
8
|
|
6
9
|
def initialize(input_file_name, output_file_name, options)
|
@@ -20,23 +23,19 @@ class Beats
|
|
20
23
|
end
|
21
24
|
|
22
25
|
song_parser = SongParser.new()
|
23
|
-
song = song_parser.parse(File.dirname(@input_file_name),
|
26
|
+
song, kit = song_parser.parse(File.dirname(@input_file_name), File.read(@input_file_name))
|
24
27
|
song_optimizer = SongOptimizer.new()
|
25
28
|
|
26
|
-
|
29
|
+
# If the -p option is used, transform the song into one whose flow consists of
|
30
|
+
# playing that single pattern once.
|
31
|
+
unless @options[:pattern] == nil
|
27
32
|
pattern_name = @options[:pattern].downcase.to_sym
|
28
|
-
|
29
|
-
raise StandardError, "The song does not include a pattern called #{@options[:pattern]}"
|
30
|
-
end
|
31
|
-
|
32
|
-
song.structure = [pattern_name]
|
33
|
-
song.remove_unused_patterns()
|
33
|
+
song.remove_patterns_except(pattern_name)
|
34
34
|
end
|
35
35
|
|
36
36
|
duration = nil
|
37
37
|
if @options[:split]
|
38
|
-
|
39
|
-
split_songs = song_splitter.split(song)
|
38
|
+
split_songs = song.split()
|
40
39
|
split_songs.each do |track_name, split_song|
|
41
40
|
split_song = song_optimizer.optimize(split_song, OPTIMIZED_PATTERN_LENGTH)
|
42
41
|
|
@@ -45,13 +44,13 @@ class Beats
|
|
45
44
|
file_name = File.dirname(@output_file_name) + "/" +
|
46
45
|
File.basename(@output_file_name, extension) + "-" + File.basename(track_name, extension) +
|
47
46
|
extension
|
48
|
-
duration = split_song.write_to_file(file_name)
|
47
|
+
duration = AudioEngine.new(split_song, kit).write_to_file(file_name)
|
49
48
|
end
|
50
49
|
else
|
51
50
|
song = song_optimizer.optimize(song, OPTIMIZED_PATTERN_LENGTH)
|
52
|
-
duration = song.write_to_file(@output_file_name)
|
51
|
+
duration = AudioEngine.new(song, kit).write_to_file(@output_file_name)
|
53
52
|
end
|
54
53
|
|
55
54
|
return {:duration => duration}
|
56
55
|
end
|
57
|
-
end
|
56
|
+
end
|
data/lib/beatswavefile.rb
CHANGED
@@ -1,15 +1,10 @@
|
|
1
1
|
# Adds some functionality to the WaveFile gem that allows for improved performance. The
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
2
|
+
# use of open_for_appending() allows a wave file to be written to disk in chunks, instead
|
3
|
+
# of all at once. This improves performance (and I would assume memory usage) by eliminating
|
4
|
+
# the need to store the entire sample data for the song in memory in a giant (i.e. millions
|
5
|
+
# of elements) array.
|
6
6
|
#
|
7
7
|
# I'm not sure these methods in their current form are suitable for the WaveFile gem.
|
8
|
-
# That's a public API so I want to be careful adding to it, and these methods fall into the
|
9
|
-
# category of "you should know what you're doing." In particular, open_for_appending() and
|
10
|
-
# write_snippet() need to be used together, and if you don't use them right your saved wave
|
11
|
-
# file will be messed up. There's probably a better API for doing this.
|
12
|
-
#
|
13
8
|
# If I figure out a better API I might add it to the WaveFile gem in the future, but until
|
14
9
|
# then I'm just putting it here. Since BEATS is a stand-alone app and not a re-usable library,
|
15
10
|
# I don't think this should be a problem.
|
@@ -17,12 +12,19 @@ class BeatsWaveFile < WaveFile
|
|
17
12
|
|
18
13
|
# Writes the header for the wave file to path, and returns an open File object that
|
19
14
|
# can be used outside the method to append the sample data. WARNING: The header contains
|
20
|
-
# a field for the total number of samples in the file. This number of samples
|
21
|
-
# subsequently be written to the file
|
22
|
-
# won't be able to play it.
|
23
|
-
def open_for_appending(path
|
15
|
+
# a field for the total number of samples in the file. This number of samples (and exactly
|
16
|
+
# this number of samples) must be subsequently be written to the file before it is closed
|
17
|
+
# or it won't be valid and you won't be able to play it.
|
18
|
+
def open_for_appending(path)
|
19
|
+
file = File.open(path, "w")
|
20
|
+
write_header(file, 0)
|
21
|
+
|
22
|
+
return file
|
23
|
+
end
|
24
|
+
|
25
|
+
def write_header(file, sample_length)
|
24
26
|
bytes_per_sample = (@bits_per_sample / 8)
|
25
|
-
sample_data_size =
|
27
|
+
sample_data_size = sample_length * bytes_per_sample * @num_channels
|
26
28
|
|
27
29
|
# Write the header
|
28
30
|
header = CHUNK_ID
|
@@ -39,29 +41,7 @@ class BeatsWaveFile < WaveFile
|
|
39
41
|
header += DATA_CHUNK_ID
|
40
42
|
header += [sample_data_size].pack("V")
|
41
43
|
|
42
|
-
file = File.open(path, "w")
|
43
44
|
file.syswrite(header)
|
44
|
-
|
45
|
-
return file
|
46
|
-
end
|
47
|
-
|
48
|
-
# Appending sample_data to file, which is assumed to be open. Should be used in
|
49
|
-
# conjunction with open_for_appending(). The File object returned by that method
|
50
|
-
# should be passed in here. WARNING: you are responsible for writing the correct
|
51
|
-
# number of samples to the file, with 1 or more calls to this method. The caller
|
52
|
-
# of this method is also responsible for closing the File object when finished.
|
53
|
-
def write_snippet(file, sample_data)
|
54
|
-
if @bits_per_sample == 8
|
55
|
-
pack_code = "C*"
|
56
|
-
elsif @bits_per_sample == 16
|
57
|
-
pack_code = "s*"
|
58
|
-
end
|
59
|
-
|
60
|
-
if @num_channels == 1
|
61
|
-
file.syswrite(sample_data.pack(pack_code))
|
62
|
-
else
|
63
|
-
file.syswrite(sample_data.flatten.pack(pack_code))
|
64
|
-
end
|
65
45
|
end
|
66
46
|
|
67
47
|
def calculate_duration(sample_rate, total_samples)
|
@@ -90,4 +70,4 @@ class BeatsWaveFile < WaveFile
|
|
90
70
|
|
91
71
|
return { :hours => hours, :minutes => minutes, :seconds => seconds, :milliseconds => milliseconds }
|
92
72
|
end
|
93
|
-
end
|
73
|
+
end
|