beats 1.2.0 → 1.2.1
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.
- 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
|