beats 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +24 -0
- data/README.markdown +27 -0
- data/bin/beats +109 -0
- data/lib/kit.rb +40 -0
- data/lib/pattern.rb +122 -0
- data/lib/song.rb +294 -0
- data/lib/track.rb +118 -0
- data/test/includes.rb +8 -0
- data/test/kit_test.rb +121 -0
- data/test/pattern_test.rb +153 -0
- data/test/song_test.rb +225 -0
- data/test/sounds/bass_mono_16.wav +0 -0
- data/test/sounds/bass_mono_8.wav +0 -0
- data/test/sounds/bass_stereo_16.wav +0 -0
- data/test/sounds/hh_closed_mono_8.wav +0 -0
- data/test/sounds/hh_open_mono_8.wav +0 -0
- data/test/sounds/ride_mono_8.wav +0 -0
- data/test/sounds/snare_mono_8.wav +0 -0
- data/test/track_test.rb +203 -0
- metadata +93 -0
data/LICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
== BEATS
|
2
|
+
|
3
|
+
# Copyright (c) 2010 Joel Strait
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person
|
6
|
+
# obtaining a copy of this software and associated documentation
|
7
|
+
# files (the "Software"), to deal in the Software without
|
8
|
+
# restriction, including without limitation the rights to use,
|
9
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
# copies of the Software, and to permit persons to whom the
|
11
|
+
# Software is furnished to do so, subject to the following
|
12
|
+
# conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be
|
15
|
+
# included in all copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
19
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
21
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
22
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
23
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
24
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
BEATS
|
2
|
+
-----
|
3
|
+
|
4
|
+
BEATS is a drum machine written in pure Ruby. Feed it a song notated in YAML, and it will produce a precision-milled Wave file of impeccable timing and feel. Here's an example song:
|
5
|
+
|
6
|
+
Song:
|
7
|
+
Tempo: 120
|
8
|
+
Structure:
|
9
|
+
- Verse: x2
|
10
|
+
- Chorus: x4
|
11
|
+
- Verse: x2
|
12
|
+
- Chorus: x4
|
13
|
+
|
14
|
+
Verse:
|
15
|
+
- bass.wav: X...X...X...X...
|
16
|
+
- snare.wav: ..............X.
|
17
|
+
- hh_closed.wav: X.XXX.XXX.X.X.X.
|
18
|
+
- agogo_high.wav: ..............XX
|
19
|
+
|
20
|
+
Chorus:
|
21
|
+
- bass.wav: X...X...X...X...
|
22
|
+
- snare.wav: ....X.......X...
|
23
|
+
- hh_closed.wav: X.XXX.XXX.XX..X.
|
24
|
+
- tom4.wav: ...........X....
|
25
|
+
- tom2.wav: ..............X.
|
26
|
+
|
27
|
+
For installation and usage instructions, visit the BEATS website at [http://beatsdrummachine.com](http://beatsdrummachine.com).
|
data/bin/beats
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + "/../lib/song"
|
4
|
+
require File.dirname(__FILE__) + "/../lib/kit"
|
5
|
+
require File.dirname(__FILE__) + "/../lib/pattern"
|
6
|
+
require File.dirname(__FILE__) + "/../lib/track"
|
7
|
+
require "rubygems"
|
8
|
+
require "optparse"
|
9
|
+
require "yaml"
|
10
|
+
require "wavefile"
|
11
|
+
|
12
|
+
BEATS_VERSION = "1.0.0"
|
13
|
+
SAMPLE_RATE = 44100
|
14
|
+
|
15
|
+
def parse_options
|
16
|
+
options = {:split => false, :pattern => ""}
|
17
|
+
|
18
|
+
optparse = OptionParser.new do |opts|
|
19
|
+
opts.on('-s', '--split', "Save each track to an individual wave file") do
|
20
|
+
options[:split] = true
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on('-p', '--pattern PATTERN_NAME', "Output a single pattern instead of the whole song" ) do |p|
|
24
|
+
options[:pattern] = p
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on('-v', '--version', "Display version number and exit") do
|
28
|
+
puts "BEATS v#{BEATS_VERSION}"
|
29
|
+
exit
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on( '-h', '--help', "Display this screen and exit" ) do
|
33
|
+
puts opts
|
34
|
+
exit
|
35
|
+
end
|
36
|
+
end
|
37
|
+
optparse.parse!
|
38
|
+
|
39
|
+
return options
|
40
|
+
end
|
41
|
+
|
42
|
+
def save_wave_file(file_name, num_channels, bits_per_sample, sample_data)
|
43
|
+
output = WaveFile.new(num_channels, SAMPLE_RATE, bits_per_sample)
|
44
|
+
output.sample_data = sample_data
|
45
|
+
output.save(file_name)
|
46
|
+
return output.duration
|
47
|
+
end
|
48
|
+
|
49
|
+
start = Time.now
|
50
|
+
|
51
|
+
options = parse_options
|
52
|
+
input_file = ARGV[0]
|
53
|
+
output_file = ARGV[1]
|
54
|
+
|
55
|
+
if(input_file == nil)
|
56
|
+
ARGV[0] = '-h'
|
57
|
+
parse_options()
|
58
|
+
end
|
59
|
+
|
60
|
+
if(output_file == nil)
|
61
|
+
output_file = File.basename(input_file, File.extname(input_file)) + ".wav"
|
62
|
+
end
|
63
|
+
|
64
|
+
begin
|
65
|
+
parse_start_time = Time.now
|
66
|
+
song_from_file = Song.new(File.dirname(input_file), YAML.load_file(input_file))
|
67
|
+
|
68
|
+
generate_samples_start = Time.now
|
69
|
+
sample_data = song_from_file.sample_data(options[:pattern], options[:split])
|
70
|
+
#puts "Time to generate sample data: #{Time.now - generate_samples_start}"
|
71
|
+
|
72
|
+
wave_write_start = Time.now
|
73
|
+
if(options[:split])
|
74
|
+
duration = nil
|
75
|
+
sample_data.keys.each {|track_name|
|
76
|
+
extension = File.extname(output_file)
|
77
|
+
file_name = File.basename(output_file, extension) + "-" + File.basename(track_name.to_s, extension) + extension
|
78
|
+
|
79
|
+
duration = save_wave_file(file_name,
|
80
|
+
song_from_file.num_channels,
|
81
|
+
song_from_file.bits_per_sample,
|
82
|
+
sample_data[track_name])
|
83
|
+
}
|
84
|
+
else
|
85
|
+
duration = save_wave_file(output_file, song_from_file.num_channels, song_from_file.bits_per_sample, sample_data)
|
86
|
+
end
|
87
|
+
|
88
|
+
puts "#{duration[:minutes]}:#{duration[:seconds].to_s.rjust(2, '0')} of audio produced in #{Time.now - start} seconds."
|
89
|
+
rescue Errno::ENOENT => detail
|
90
|
+
puts ""
|
91
|
+
puts "Song file '#{input_file}' not found."
|
92
|
+
puts ""
|
93
|
+
rescue ArgumentError => detail
|
94
|
+
puts ""
|
95
|
+
puts "Song file '#{input_file}' has an error:"
|
96
|
+
puts " Syntax error in YAML file:"
|
97
|
+
puts " #{detail}"
|
98
|
+
puts ""
|
99
|
+
rescue SongParseError => detail
|
100
|
+
puts ""
|
101
|
+
puts "Song file '#{input_file}' has an error:"
|
102
|
+
puts " #{detail}"
|
103
|
+
puts ""
|
104
|
+
rescue StandardError => detail
|
105
|
+
puts ""
|
106
|
+
puts "An error occured while generating sound for '#{input_file}':"
|
107
|
+
puts " #{detail}"
|
108
|
+
puts ""
|
109
|
+
end
|
data/lib/kit.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
class Kit
|
2
|
+
def initialize()
|
3
|
+
@sounds = {}
|
4
|
+
@num_channels = 0
|
5
|
+
@bits_per_sample = 0
|
6
|
+
end
|
7
|
+
|
8
|
+
def add(name, path)
|
9
|
+
if(!@sounds.has_key? name)
|
10
|
+
w = WaveFile.open(path)
|
11
|
+
@sounds[name] = w
|
12
|
+
|
13
|
+
if w.num_channels > @num_channels
|
14
|
+
@num_channels = w.num_channels
|
15
|
+
end
|
16
|
+
if w.bits_per_sample > @bits_per_sample
|
17
|
+
@bits_per_sample = w.bits_per_sample
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_sample_data(name)
|
23
|
+
w = @sounds[name]
|
24
|
+
|
25
|
+
if w == nil
|
26
|
+
raise StandardError, "Kit doesn't contain sound '#{name}'."
|
27
|
+
else
|
28
|
+
w.num_channels = @num_channels
|
29
|
+
w.bits_per_sample = @bits_per_sample
|
30
|
+
|
31
|
+
return w.sample_data
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def size
|
36
|
+
return @sounds.length
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :bits_per_sample, :num_channels
|
40
|
+
end
|
data/lib/pattern.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
class Pattern
|
2
|
+
def initialize(name)
|
3
|
+
@name = name
|
4
|
+
@tracks = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def track(name, wave_data, pattern)
|
8
|
+
new_track = Track.new(name, wave_data, pattern)
|
9
|
+
@tracks[new_track.name] = new_track
|
10
|
+
|
11
|
+
# If the new track is longer than any of the previously added tracks,
|
12
|
+
# pad the other tracks with trailing . to make them all the same length.
|
13
|
+
# Necessary to prevent incorrect overflow calculations for tracks.
|
14
|
+
longest_track_length = 0
|
15
|
+
@tracks.values.each {|track|
|
16
|
+
if(track.pattern.length > longest_track_length)
|
17
|
+
longest_track_length = track.pattern.length
|
18
|
+
end
|
19
|
+
}
|
20
|
+
@tracks.values.each {|track|
|
21
|
+
if(track.pattern.length < longest_track_length)
|
22
|
+
track.pattern += "." * (longest_track_length - track.pattern.length)
|
23
|
+
end
|
24
|
+
}
|
25
|
+
|
26
|
+
return new_track
|
27
|
+
end
|
28
|
+
|
29
|
+
def sample_length(tick_sample_length)
|
30
|
+
@tracks.keys.collect {|track_name| @tracks[track_name].sample_length(tick_sample_length) }.max || 0
|
31
|
+
end
|
32
|
+
|
33
|
+
def sample_length_with_overflow(tick_sample_length)
|
34
|
+
@tracks.keys.collect {|track_name| @tracks[track_name].sample_length_with_overflow(tick_sample_length) }.max || 0
|
35
|
+
end
|
36
|
+
|
37
|
+
def sample_data(tick_sample_length, num_channels, num_tracks_in_song, incoming_overflow, split = false)
|
38
|
+
if(split)
|
39
|
+
return split_sample_data(tick_sample_length, num_channels, incoming_overflow)
|
40
|
+
else
|
41
|
+
return combined_sample_data(tick_sample_length, num_channels, num_tracks_in_song, incoming_overflow)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_accessor :tracks, :name
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def combined_sample_data(tick_sample_length, num_channels, num_tracks_in_song, incoming_overflow)
|
50
|
+
fill_value = (num_channels == 1) ? 0 : [].fill(0, 0, num_channels)
|
51
|
+
track_names = @tracks.keys
|
52
|
+
primary_sample_data = []
|
53
|
+
overflow_sample_data = {}
|
54
|
+
actual_sample_length = sample_length(tick_sample_length)
|
55
|
+
|
56
|
+
if(track_names.length > 0)
|
57
|
+
primary_sample_data = [].fill(fill_value, 0, actual_sample_length)
|
58
|
+
|
59
|
+
track_names.each {|track_name|
|
60
|
+
temp = @tracks[track_name].sample_data(tick_sample_length, incoming_overflow[track_name])
|
61
|
+
|
62
|
+
track_samples = temp[:primary]
|
63
|
+
if(num_channels == 1)
|
64
|
+
(0...track_samples.length).each {|i| primary_sample_data[i] += track_samples[i] }
|
65
|
+
else
|
66
|
+
(0...track_samples.length).each {|i|
|
67
|
+
primary_sample_data[i] = [primary_sample_data[i][0] + track_samples[i][0],
|
68
|
+
primary_sample_data[i][1] + track_samples[i][1]]
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
overflow_sample_data[track_name] = temp[:overflow]
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
# Add samples for tracks with overflow from previous pattern, but not
|
77
|
+
# contained in current pattern.
|
78
|
+
incoming_overflow.keys.each {|track_name|
|
79
|
+
if(!track_names.member?(track_name) && incoming_overflow[track_name].length > 0)
|
80
|
+
if(num_channels == 1)
|
81
|
+
(0...incoming_overflow[track_name].length).each {|i| primary_sample_data[i] += incoming_overflow[track_name][i]}
|
82
|
+
else
|
83
|
+
(0...incoming_overflow[track_name].length).each {|i| primary_sample_data[i][0] += incoming_overflow[track_name][i][0]
|
84
|
+
primary_sample_data[i][1] += incoming_overflow[track_name][i][1]}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
}
|
88
|
+
|
89
|
+
# Mix down the tracks into one
|
90
|
+
if(num_channels == 1)
|
91
|
+
primary_sample_data = primary_sample_data.map {|sample| (sample / num_tracks_in_song).round }
|
92
|
+
else
|
93
|
+
primary_sample_data = primary_sample_data.map {|sample| [(sample[0] / num_tracks_in_song).round, (sample[1] / num_tracks_in_song).round] }
|
94
|
+
end
|
95
|
+
|
96
|
+
return {:primary => primary_sample_data, :overflow => overflow_sample_data}
|
97
|
+
end
|
98
|
+
|
99
|
+
def split_sample_data(tick_sample_length, num_channels, incoming_overflow)
|
100
|
+
fill_value = (num_channels == 1) ? 0 : [].fill(0, 0, num_channels)
|
101
|
+
primary_sample_data = {}
|
102
|
+
overflow_sample_data = {}
|
103
|
+
|
104
|
+
@tracks.keys.each {|track_name|
|
105
|
+
temp = @tracks[track_name].sample_data(tick_sample_length, incoming_overflow[track_name])
|
106
|
+
primary_sample_data[track_name] = temp[:primary]
|
107
|
+
overflow_sample_data[track_name] = temp[:overflow]
|
108
|
+
}
|
109
|
+
|
110
|
+
incoming_overflow.keys.each {|track_name|
|
111
|
+
if(@tracks[track_name] == nil)
|
112
|
+
# TO DO: Add check for when incoming overflow is longer than
|
113
|
+
# track full length to prevent track from lengthening.
|
114
|
+
primary_sample_data[track_name] = [].fill(fill_value, 0, sample_length(tick_sample_length))
|
115
|
+
primary_sample_data[track_name][0...incoming_overflow[track_name].length] = incoming_overflow[track_name]
|
116
|
+
overflow_sample_data[track_name] = []
|
117
|
+
end
|
118
|
+
}
|
119
|
+
|
120
|
+
return {:primary => primary_sample_data, :overflow => overflow_sample_data}
|
121
|
+
end
|
122
|
+
end
|
data/lib/song.rb
ADDED
@@ -0,0 +1,294 @@
|
|
1
|
+
class SongParseError < RuntimeError; end
|
2
|
+
|
3
|
+
class Song
|
4
|
+
SAMPLE_RATE = 44100
|
5
|
+
SECONDS_PER_MINUTE = 60.0
|
6
|
+
PATH_SEPARATOR = File.const_get("SEPARATOR")
|
7
|
+
|
8
|
+
def initialize(input_path, definition = nil)
|
9
|
+
self.tempo = 120
|
10
|
+
@input_path = input_path
|
11
|
+
@kit = Kit.new()
|
12
|
+
@patterns = {}
|
13
|
+
@structure = []
|
14
|
+
|
15
|
+
if(definition != nil)
|
16
|
+
parse(definition)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def pattern(name)
|
21
|
+
@patterns[name] = Pattern.new(name)
|
22
|
+
return @patterns[name]
|
23
|
+
end
|
24
|
+
|
25
|
+
def sample_length()
|
26
|
+
@structure.inject(0) {|sum, pattern_name|
|
27
|
+
sum + @patterns[pattern_name].sample_length(@tick_sample_length)
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def sample_length_with_overflow()
|
32
|
+
if(@structure.length == 0)
|
33
|
+
return 0
|
34
|
+
end
|
35
|
+
|
36
|
+
full_sample_length = self.sample_length
|
37
|
+
last_pattern_sample_length = @patterns[@structure.last].sample_length(@tick_sample_length)
|
38
|
+
last_pattern_overflow_length = @patterns[@structure.last].sample_length_with_overflow(@tick_sample_length)
|
39
|
+
overflow = last_pattern_overflow_length - last_pattern_sample_length
|
40
|
+
|
41
|
+
return sample_length + overflow
|
42
|
+
end
|
43
|
+
|
44
|
+
def total_tracks()
|
45
|
+
@patterns.keys.collect {|pattern_name| @patterns[pattern_name].tracks.length }.max || 0
|
46
|
+
end
|
47
|
+
|
48
|
+
def sample_data(pattern_name, split)
|
49
|
+
num_tracks_in_song = self.total_tracks()
|
50
|
+
fill_value = (@kit.num_channels == 1) ? 0 : [].fill(0, 0, @kit.num_channels)
|
51
|
+
|
52
|
+
if(pattern_name == "")
|
53
|
+
if(split)
|
54
|
+
return sample_data_split_all_patterns(fill_value, num_tracks_in_song)
|
55
|
+
else
|
56
|
+
return sample_data_combined_all_patterns(fill_value, num_tracks_in_song)
|
57
|
+
end
|
58
|
+
else
|
59
|
+
pattern = @patterns[pattern_name.downcase.to_sym]
|
60
|
+
|
61
|
+
if(pattern == nil)
|
62
|
+
raise StandardError, "Pattern '#{pattern_name}' not found in song."
|
63
|
+
else
|
64
|
+
primary_sample_length = pattern.sample_length(@tick_sample_length)
|
65
|
+
|
66
|
+
if(split)
|
67
|
+
return sample_data_split_single_pattern(fill_value, num_tracks_in_song, pattern, primary_sample_length)
|
68
|
+
else
|
69
|
+
return sample_data_combined_single_pattern(fill_value, num_tracks_in_song, pattern, primary_sample_length)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def num_channels()
|
76
|
+
return @kit.num_channels
|
77
|
+
end
|
78
|
+
|
79
|
+
def bits_per_sample()
|
80
|
+
return @kit.bits_per_sample
|
81
|
+
end
|
82
|
+
|
83
|
+
def tempo()
|
84
|
+
return @tempo
|
85
|
+
end
|
86
|
+
|
87
|
+
def tempo=(new_tempo)
|
88
|
+
if(new_tempo.class != Fixnum || new_tempo <= 0)
|
89
|
+
raise SongParseError, "Invalid tempo: '#{new_tempo}'. Tempo must be a number greater than 0."
|
90
|
+
end
|
91
|
+
|
92
|
+
@tempo = new_tempo
|
93
|
+
@tick_sample_length = (SAMPLE_RATE * SECONDS_PER_MINUTE) / new_tempo / 4.0
|
94
|
+
end
|
95
|
+
|
96
|
+
attr_reader :input_path, :tick_sample_length
|
97
|
+
attr_accessor :structure
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def merge_overflow(overflow, num_tracks_in_song)
|
102
|
+
merged_sample_data = []
|
103
|
+
|
104
|
+
if(overflow != {})
|
105
|
+
longest_overflow = overflow[overflow.keys.first]
|
106
|
+
overflow.keys.each {|track_name|
|
107
|
+
if(overflow[track_name].length > longest_overflow.length)
|
108
|
+
longest_overflow = overflow[track_name]
|
109
|
+
end
|
110
|
+
}
|
111
|
+
|
112
|
+
final_overflow_pattern = Pattern.new(:overflow)
|
113
|
+
final_overflow_pattern.track "", [], "."
|
114
|
+
final_overflow_sample_data = final_overflow_pattern.sample_data(longest_overflow.length, @kit.num_channels, num_tracks_in_song, overflow, false)
|
115
|
+
merged_sample_data = final_overflow_sample_data[:primary]
|
116
|
+
end
|
117
|
+
|
118
|
+
return merged_sample_data
|
119
|
+
end
|
120
|
+
|
121
|
+
# Converts all hash keys to be lowercase
|
122
|
+
def downcase_hash_keys(hash)
|
123
|
+
return hash.inject({}) {|new_hash, pair|
|
124
|
+
new_hash[pair.first.downcase] = pair.last
|
125
|
+
new_hash
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
def parse(definition)
|
130
|
+
if(definition.class == String)
|
131
|
+
song_definition = YAML.load(definition)
|
132
|
+
elsif(definition.class == Hash)
|
133
|
+
song_definition = definition
|
134
|
+
else
|
135
|
+
raise StandardError, "Invalid song input"
|
136
|
+
end
|
137
|
+
|
138
|
+
@kit = build_kit(song_definition)
|
139
|
+
|
140
|
+
song_definition = downcase_hash_keys(song_definition)
|
141
|
+
|
142
|
+
# Process each pattern
|
143
|
+
song_definition.keys.each{|key|
|
144
|
+
if(key != "song")
|
145
|
+
new_pattern = self.pattern key.to_sym
|
146
|
+
|
147
|
+
track_list = song_definition[key]
|
148
|
+
track_list.each{|track_definition|
|
149
|
+
track_name = track_definition.keys.first
|
150
|
+
new_pattern.track track_name, @kit.get_sample_data(track_name), track_definition[track_name]
|
151
|
+
}
|
152
|
+
end
|
153
|
+
}
|
154
|
+
|
155
|
+
# Process song header
|
156
|
+
parse_song_header(downcase_hash_keys(song_definition["song"]))
|
157
|
+
end
|
158
|
+
|
159
|
+
def parse_song_header(header_data)
|
160
|
+
self.tempo = header_data["tempo"]
|
161
|
+
|
162
|
+
pattern_list = header_data["structure"]
|
163
|
+
structure = []
|
164
|
+
pattern_list.each{|pattern_item|
|
165
|
+
if(pattern_item.class == String)
|
166
|
+
pattern_item = {pattern_item => "x1"}
|
167
|
+
end
|
168
|
+
|
169
|
+
pattern_name = pattern_item.keys.first
|
170
|
+
pattern_name_sym = pattern_name.downcase.to_sym
|
171
|
+
|
172
|
+
if(!@patterns.has_key?(pattern_name_sym))
|
173
|
+
raise SongParseError, "Song structure includes non-existant pattern: #{pattern_name}."
|
174
|
+
end
|
175
|
+
|
176
|
+
multiples_str = pattern_item[pattern_name]
|
177
|
+
multiples_str.slice!(0)
|
178
|
+
multiples = multiples_str.to_i
|
179
|
+
|
180
|
+
if(multiples_str.match(/[^0-9]/) != nil)
|
181
|
+
raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Number of repeats should be a whole number."
|
182
|
+
elsif(multiples < 0)
|
183
|
+
raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Must be 0 or greater."
|
184
|
+
end
|
185
|
+
|
186
|
+
multiples.times { structure << pattern_name_sym }
|
187
|
+
}
|
188
|
+
|
189
|
+
@structure = structure
|
190
|
+
end
|
191
|
+
|
192
|
+
def build_kit(song_definition)
|
193
|
+
kit = Kit.new()
|
194
|
+
|
195
|
+
song_definition.keys.each{|key|
|
196
|
+
if(key.downcase != "song")
|
197
|
+
track_list = song_definition[key]
|
198
|
+
track_list.each{|track_definition|
|
199
|
+
track_name = track_definition.keys.first
|
200
|
+
track_path = track_name
|
201
|
+
if(!track_path.start_with?(PATH_SEPARATOR))
|
202
|
+
track_path = @input_path + PATH_SEPARATOR + track_path
|
203
|
+
end
|
204
|
+
|
205
|
+
if(!File.exists? track_path)
|
206
|
+
raise SongParseError, "File '#{track_name}' not found for pattern '#{key}'"
|
207
|
+
end
|
208
|
+
|
209
|
+
kit.add(track_name, track_path)
|
210
|
+
}
|
211
|
+
end
|
212
|
+
}
|
213
|
+
|
214
|
+
return kit
|
215
|
+
end
|
216
|
+
|
217
|
+
def sample_data_split_all_patterns(fill_value, num_tracks_in_song)
|
218
|
+
output_data = {}
|
219
|
+
|
220
|
+
offset = 0
|
221
|
+
overflow = {}
|
222
|
+
@structure.each {|pattern_name|
|
223
|
+
pattern_sample_length = @patterns[pattern_name].sample_length(@tick_sample_length)
|
224
|
+
pattern_sample_data = @patterns[pattern_name].sample_data(@tick_sample_length, @kit.num_channels, num_tracks_in_song, overflow, true)
|
225
|
+
|
226
|
+
pattern_sample_data[:primary].keys.each {|track_name|
|
227
|
+
if(output_data[track_name] == nil)
|
228
|
+
output_data[track_name] = [].fill(fill_value, 0, self.sample_length_with_overflow())
|
229
|
+
end
|
230
|
+
|
231
|
+
output_data[track_name][offset...(offset + pattern_sample_length)] = pattern_sample_data[:primary][track_name]
|
232
|
+
}
|
233
|
+
|
234
|
+
overflow.keys.each {|track_name|
|
235
|
+
if(pattern_sample_data[:primary][track_name] == nil)
|
236
|
+
output_data[track_name][offset...overflow[track_name].length] = overflow[track_name]
|
237
|
+
end
|
238
|
+
}
|
239
|
+
|
240
|
+
overflow = pattern_sample_data[:overflow]
|
241
|
+
offset += pattern_sample_length
|
242
|
+
}
|
243
|
+
|
244
|
+
overflow.keys.each {|track_name|
|
245
|
+
output_data[track_name][offset...overflow[track_name].length] = overflow[track_name]
|
246
|
+
}
|
247
|
+
|
248
|
+
return output_data
|
249
|
+
end
|
250
|
+
|
251
|
+
def sample_data_split_single_pattern(fill_value, num_tracks_in_song, pattern, primary_sample_length)
|
252
|
+
output_data = {}
|
253
|
+
|
254
|
+
pattern_sample_length = pattern.sample_length(@tick_sample_length)
|
255
|
+
pattern_sample_data = pattern.sample_data(@tick_sample_length, @kit.num_channels, num_tracks_in_song, {}, true)
|
256
|
+
|
257
|
+
pattern_sample_data[:primary].keys.each {|track_name|
|
258
|
+
overflow_sample_length = pattern_sample_data[:overflow][track_name].length
|
259
|
+
full_sample_length = pattern_sample_length + overflow_sample_length
|
260
|
+
output_data[track_name] = [].fill(fill_value, 0, full_sample_length)
|
261
|
+
output_data[track_name][0...pattern_sample_length] = pattern_sample_data[:primary][track_name]
|
262
|
+
output_data[track_name][pattern_sample_length...full_sample_length] = pattern_sample_data[:overflow][track_name]
|
263
|
+
}
|
264
|
+
|
265
|
+
return output_data
|
266
|
+
end
|
267
|
+
|
268
|
+
def sample_data_combined_all_patterns(fill_value, num_tracks_in_song)
|
269
|
+
output_data = [].fill(fill_value, 0, self.sample_length_with_overflow)
|
270
|
+
|
271
|
+
offset = 0
|
272
|
+
overflow = {}
|
273
|
+
@structure.each {|pattern_name|
|
274
|
+
pattern_sample_length = @patterns[pattern_name].sample_length(@tick_sample_length)
|
275
|
+
pattern_sample_data = @patterns[pattern_name].sample_data(@tick_sample_length, @kit.num_channels, num_tracks_in_song, overflow)
|
276
|
+
output_data[offset...offset + pattern_sample_length] = pattern_sample_data[:primary]
|
277
|
+
overflow = pattern_sample_data[:overflow]
|
278
|
+
offset += pattern_sample_length
|
279
|
+
}
|
280
|
+
|
281
|
+
# Handle overflow from final pattern
|
282
|
+
output_data[offset...output_data.length] = merge_overflow(overflow, num_tracks_in_song)
|
283
|
+
return output_data
|
284
|
+
end
|
285
|
+
|
286
|
+
def sample_data_combined_single_pattern(fill_value, num_tracks_in_song, pattern, primary_sample_length)
|
287
|
+
output_data = [].fill(fill_value, 0, pattern.sample_length_with_overflow(@tick_sample_length))
|
288
|
+
sample_data = pattern.sample_data(tick_sample_length, @kit.num_channels, num_tracks_in_song, {}, false)
|
289
|
+
output_data[0...primary_sample_length] = sample_data[:primary]
|
290
|
+
output_data[primary_sample_length...output_data.length] = merge_overflow(sample_data[:overflow], num_tracks_in_song)
|
291
|
+
|
292
|
+
return output_data
|
293
|
+
end
|
294
|
+
end
|