beats 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +29 -4
- data/bin/beats +36 -71
- data/lib/beats.rb +57 -0
- data/lib/beatswavefile.rb +93 -0
- data/lib/kit.rb +67 -11
- data/lib/pattern.rb +131 -86
- data/lib/song.rb +145 -114
- data/lib/songoptimizer.rb +154 -0
- data/lib/songparser.rb +40 -28
- data/lib/songsplitter.rb +38 -0
- data/lib/track.rb +33 -31
- data/lib/wavefile.rb +475 -0
- data/test/examples/combined.wav +0 -0
- data/test/examples/split-agogo_high.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/test/examples/split-tom4.wav +0 -0
- data/test/fixtures/expected_output/example_combined_mono_16.wav +0 -0
- data/test/fixtures/expected_output/example_combined_mono_8.wav +0 -0
- data/test/fixtures/expected_output/example_combined_stereo_16.wav +0 -0
- data/test/fixtures/expected_output/example_combined_stereo_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-bass.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-tom2_mono_16.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-tom4_mono_16.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-bass.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-tom2_mono_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-tom4_mono_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-bass.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-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-tom2_stereo_16.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-tom4_stereo_16.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-bass.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-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-tom2_stereo_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-tom4_stereo_8.wav +0 -0
- data/test/fixtures/invalid/bad_repeat_count.txt +8 -0
- data/test/fixtures/invalid/bad_rhythm.txt +9 -0
- data/test/fixtures/invalid/bad_structure.txt +9 -0
- data/test/fixtures/invalid/bad_tempo.txt +8 -0
- data/test/fixtures/invalid/no_header.txt +3 -0
- data/test/fixtures/invalid/no_structure.txt +6 -0
- data/test/fixtures/invalid/pattern_with_no_tracks.txt +12 -0
- data/test/fixtures/invalid/sound_in_kit_not_found.txt +10 -0
- data/test/fixtures/invalid/sound_in_track_not_found.txt +8 -0
- data/test/fixtures/invalid/template.txt +31 -0
- data/test/fixtures/valid/example_mono_16.txt +28 -0
- data/test/fixtures/valid/example_mono_8.txt +28 -0
- data/test/fixtures/valid/example_no_kit.txt +30 -0
- data/test/fixtures/valid/example_stereo_16.txt +28 -0
- data/test/fixtures/valid/example_stereo_8.txt +28 -0
- data/test/fixtures/valid/example_with_empty_track.txt +10 -0
- data/test/fixtures/valid/example_with_kit.txt +34 -0
- data/test/fixtures/valid/no_tempo.txt +8 -0
- data/test/fixtures/valid/pattern_with_overflow.txt +9 -0
- data/test/fixtures/valid/repeats_not_specified.txt +10 -0
- data/test/fixtures/yaml/song_yaml.txt +30 -0
- data/test/includes.rb +11 -4
- data/test/integration.rb +100 -0
- data/test/kit_test.rb +39 -39
- data/test/pattern_test.rb +119 -71
- data/test/song_test.rb +87 -62
- data/test/songoptimizer_test.rb +162 -0
- data/test/songparser_test.rb +36 -165
- data/test/sounds/agogo_high_mono_16.wav +0 -0
- data/test/sounds/agogo_high_mono_8.wav +0 -0
- data/test/sounds/agogo_high_stereo_16.wav +0 -0
- data/test/sounds/agogo_high_stereo_8.wav +0 -0
- data/test/sounds/agogo_low_mono_16.wav +0 -0
- data/test/sounds/agogo_low_mono_8.wav +0 -0
- data/test/sounds/agogo_low_stereo_16.wav +0 -0
- data/test/sounds/agogo_low_stereo_8.wav +0 -0
- data/test/sounds/bass2_mono_16.wav +0 -0
- data/test/sounds/bass2_mono_8.wav +0 -0
- data/test/sounds/bass2_stereo_16.wav +0 -0
- data/test/sounds/bass2_stereo_8.wav +0 -0
- data/test/sounds/bass_mono_8.wav +0 -0
- data/test/sounds/bass_stereo_16.wav +0 -0
- data/test/sounds/bass_stereo_8.wav +0 -0
- data/test/sounds/clave_high_mono_16.wav +0 -0
- data/test/sounds/clave_high_mono_8.wav +0 -0
- data/test/sounds/clave_high_stereo_16.wav +0 -0
- data/test/sounds/clave_high_stereo_8.wav +0 -0
- data/test/sounds/clave_low_mono_16.wav +0 -0
- data/test/sounds/clave_low_mono_8.wav +0 -0
- data/test/sounds/clave_low_stereo_16.wav +0 -0
- data/test/sounds/clave_low_stereo_8.wav +0 -0
- data/test/sounds/conga_high_mono_16.wav +0 -0
- data/test/sounds/conga_high_mono_8.wav +0 -0
- data/test/sounds/conga_high_stereo_16.wav +0 -0
- data/test/sounds/conga_high_stereo_8.wav +0 -0
- data/test/sounds/conga_low_mono_16.wav +0 -0
- data/test/sounds/conga_low_mono_8.wav +0 -0
- data/test/sounds/conga_low_stereo_16.wav +0 -0
- data/test/sounds/conga_low_stereo_8.wav +0 -0
- data/test/sounds/cowbell_high_mono_16.wav +0 -0
- data/test/sounds/cowbell_high_mono_8.wav +0 -0
- data/test/sounds/cowbell_high_stereo_16.wav +0 -0
- data/test/sounds/cowbell_high_stereo_8.wav +0 -0
- data/test/sounds/cowbell_low_mono_16.wav +0 -0
- data/test/sounds/cowbell_low_mono_8.wav +0 -0
- data/test/sounds/cowbell_low_stereo_16.wav +0 -0
- data/test/sounds/cowbell_low_stereo_8.wav +0 -0
- data/test/sounds/hh_closed_mono_16.wav +0 -0
- data/test/sounds/hh_closed_mono_8.wav +0 -0
- data/test/sounds/hh_closed_stereo_16.wav +0 -0
- data/test/sounds/hh_closed_stereo_8.wav +0 -0
- data/test/sounds/hh_open_mono_16.wav +0 -0
- data/test/sounds/hh_open_mono_8.wav +0 -0
- data/test/sounds/hh_open_stereo_16.wav +0 -0
- data/test/sounds/hh_open_stereo_8.wav +0 -0
- data/test/sounds/ride_mono_16.wav +0 -0
- data/test/sounds/ride_mono_8.wav +0 -0
- data/test/sounds/ride_stereo_16.wav +0 -0
- data/test/sounds/ride_stereo_8.wav +0 -0
- data/test/sounds/rim_mono_16.wav +0 -0
- data/test/sounds/rim_mono_8.wav +0 -0
- data/test/sounds/rim_stereo_16.wav +0 -0
- data/test/sounds/rim_stereo_8.wav +0 -0
- data/test/sounds/sine-mono-8bit.wav +0 -0
- data/test/sounds/snare2_mono_16.wav +0 -0
- data/test/sounds/snare2_mono_8.wav +0 -0
- data/test/sounds/snare2_stereo_16.wav +0 -0
- data/test/sounds/snare2_stereo_8.wav +0 -0
- data/test/sounds/snare_mono_16.wav +0 -0
- data/test/sounds/snare_mono_8.wav +0 -0
- data/test/sounds/snare_stereo_16.wav +0 -0
- data/test/sounds/snare_stereo_8.wav +0 -0
- data/test/sounds/tom1_mono_16.wav +0 -0
- data/test/sounds/tom1_mono_8.wav +0 -0
- data/test/sounds/tom1_stereo_16.wav +0 -0
- data/test/sounds/tom1_stereo_8.wav +0 -0
- data/test/sounds/tom2_mono_16.wav +0 -0
- data/test/sounds/tom2_mono_8.wav +0 -0
- data/test/sounds/tom2_stereo_16.wav +0 -0
- data/test/sounds/tom2_stereo_8.wav +0 -0
- data/test/sounds/tom3_mono_16.wav +0 -0
- data/test/sounds/tom3_mono_8.wav +0 -0
- data/test/sounds/tom3_stereo_16.wav +0 -0
- data/test/sounds/tom3_stereo_8.wav +0 -0
- data/test/sounds/tom4_mono_16.wav +0 -0
- data/test/sounds/tom4_mono_8.wav +0 -0
- data/test/sounds/tom4_stereo_16.wav +0 -0
- data/test/sounds/tom4_stereo_8.wav +0 -0
- data/test/sounds/tone.wav +0 -0
- data/test/track_test.rb +78 -72
- metadata +277 -15
data/README.markdown
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
BEATS
|
2
|
-
|
1
|
+
BEATS Drum Machine
|
2
|
+
------------------
|
3
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
|
4
|
+
BEATS is a command-line drum machine written in pure Ruby. Feed it a song notated in YAML, and it will produce a precision-milled *.wav file of impeccable timing and feel. Here's an example song:
|
5
5
|
|
6
6
|
Song:
|
7
7
|
Tempo: 120
|
@@ -29,4 +29,29 @@ BEATS is a drum machine written in pure Ruby. Feed it a song notated in YAML, an
|
|
29
29
|
- sounds/tom4.wav: ...........X....
|
30
30
|
- sounds/tom2.wav: ..............X.
|
31
31
|
|
32
|
-
|
32
|
+
And [here is what it sounds like](http://beatsdrummachine.com/beat.mp3) after getting the BEATS treatment. What a glorious groove!
|
33
|
+
|
34
|
+
Current Status
|
35
|
+
--------------
|
36
|
+
|
37
|
+
The latest stable version of BEATS is 1.2.0. It was just released! It brings significant performance and architectural improvements. It also contains a few bug fixes.
|
38
|
+
|
39
|
+
Since it was just released, I'm not sure just yet what will be coming in 1.3.0.
|
40
|
+
|
41
|
+
|
42
|
+
Installation
|
43
|
+
------------
|
44
|
+
|
45
|
+
To install the latest stable version (1.2.0), run the following from the command line:
|
46
|
+
|
47
|
+
sudo gem install beats
|
48
|
+
|
49
|
+
You can then run BEATS from the command-line using the `beats` command.
|
50
|
+
|
51
|
+
BEATS is not very useful unless you have some sounds to use with it. You can download some example sounds from [http://beatsdrummachine.com](http://beatsdrummachine.com).
|
52
|
+
|
53
|
+
|
54
|
+
Usage
|
55
|
+
-----
|
56
|
+
|
57
|
+
BEATS runs from the command-line. Run `beats -h` to see the available options. For more detailed instructions, visit [http://beatsdrummachine.com](http://beatsdrummachine.com)
|
data/bin/beats
CHANGED
@@ -1,20 +1,23 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
require File.dirname(__FILE__) + "/../lib/pattern"
|
7
|
-
require File.dirname(__FILE__) + "/../lib/track"
|
8
|
-
require "rubygems"
|
3
|
+
start_time = Time.now
|
4
|
+
|
5
|
+
$:.unshift File.dirname(__FILE__) + "/.."
|
9
6
|
require "optparse"
|
10
7
|
require "yaml"
|
11
|
-
require "
|
12
|
-
|
13
|
-
|
14
|
-
|
8
|
+
require "lib/beats"
|
9
|
+
require "lib/song"
|
10
|
+
require "lib/songparser"
|
11
|
+
require "lib/songoptimizer"
|
12
|
+
require "lib/songsplitter"
|
13
|
+
require "lib/kit"
|
14
|
+
require "lib/pattern"
|
15
|
+
require "lib/track"
|
16
|
+
require "lib/wavefile"
|
17
|
+
require "lib/beatswavefile"
|
15
18
|
|
16
19
|
def parse_options
|
17
|
-
options = {:split => false, :pattern =>
|
20
|
+
options = {:split => false, :pattern => nil}
|
18
21
|
|
19
22
|
optparse = OptionParser.new do |opts|
|
20
23
|
opts.on('-s', '--split', "Save each track to an individual wave file") do
|
@@ -24,82 +27,44 @@ def parse_options
|
|
24
27
|
opts.on('-p', '--pattern PATTERN_NAME', "Output a single pattern instead of the whole song" ) do |p|
|
25
28
|
options[:pattern] = p
|
26
29
|
end
|
27
|
-
|
30
|
+
|
28
31
|
opts.on('-v', '--version', "Display version number and exit") do
|
29
|
-
puts "BEATS v#{BEATS_VERSION}"
|
32
|
+
puts "BEATS v#{Beats::BEATS_VERSION}"
|
30
33
|
exit
|
31
34
|
end
|
32
|
-
|
35
|
+
|
33
36
|
opts.on( '-h', '--help', "Display this screen and exit" ) do
|
34
37
|
puts opts
|
35
38
|
exit
|
36
39
|
end
|
37
40
|
end
|
38
41
|
optparse.parse!
|
39
|
-
|
40
|
-
return options
|
41
|
-
end
|
42
42
|
|
43
|
-
|
44
|
-
output = WaveFile.new(num_channels, SAMPLE_RATE, bits_per_sample)
|
45
|
-
output.sample_data = sample_data
|
46
|
-
output.save(file_name)
|
47
|
-
return output.duration
|
43
|
+
return options
|
48
44
|
end
|
49
45
|
|
50
|
-
start = Time.now
|
51
|
-
|
52
46
|
options = parse_options
|
53
|
-
|
54
|
-
|
47
|
+
input_file_name = ARGV[0]
|
48
|
+
output_file_name = ARGV[1]
|
55
49
|
|
56
|
-
|
57
|
-
ARGV[0] = '-h'
|
58
|
-
parse_options()
|
59
|
-
end
|
60
|
-
|
61
|
-
if(output_file == nil)
|
62
|
-
output_file = File.basename(input_file, File.extname(input_file)) + ".wav"
|
63
|
-
end
|
50
|
+
beats = Beats.new(input_file_name, output_file_name, options)
|
64
51
|
|
65
52
|
begin
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
generate_samples_start = Time.now
|
71
|
-
sample_data = song_from_file.sample_data(options[:pattern], options[:split])
|
72
|
-
#puts "Time to generate sample data: #{Time.now - generate_samples_start}"
|
73
|
-
|
74
|
-
wave_write_start = Time.now
|
75
|
-
if(options[:split])
|
76
|
-
duration = nil
|
77
|
-
sample_data.keys.each {|track_name|
|
78
|
-
extension = File.extname(output_file)
|
79
|
-
file_name = File.basename(output_file, extension) + "-" + File.basename(track_name.to_s, extension) + extension
|
80
|
-
|
81
|
-
duration = save_wave_file(file_name,
|
82
|
-
song_from_file.num_channels,
|
83
|
-
song_from_file.bits_per_sample,
|
84
|
-
sample_data[track_name])
|
85
|
-
}
|
86
|
-
else
|
87
|
-
duration = save_wave_file(output_file, song_from_file.num_channels, song_from_file.bits_per_sample, sample_data)
|
88
|
-
end
|
89
|
-
|
90
|
-
puts "#{duration[:minutes]}:#{duration[:seconds].to_s.rjust(2, '0')} of audio produced in #{Time.now - start} seconds."
|
53
|
+
output = beats.run()
|
54
|
+
duration = output[:duration]
|
55
|
+
puts "#{duration[:minutes]}:#{duration[:seconds].to_s.rjust(2, '0')} of audio written in #{Time.now - start_time} seconds."
|
91
56
|
rescue Errno::ENOENT => detail
|
92
|
-
puts ""
|
93
|
-
puts "Song file '#{
|
94
|
-
puts ""
|
57
|
+
puts "\n"
|
58
|
+
puts "Song file '#{input_file_name}' not found.\n"
|
59
|
+
puts "\n"
|
95
60
|
rescue SongParseError => detail
|
96
|
-
puts ""
|
97
|
-
puts "Song file '#{
|
98
|
-
puts " #{detail}"
|
99
|
-
puts ""
|
61
|
+
puts "\n"
|
62
|
+
puts "Song file '#{input_file_name}' has an error:\n"
|
63
|
+
puts " #{detail}\n"
|
64
|
+
puts "\n"
|
100
65
|
rescue StandardError => detail
|
101
|
-
puts ""
|
102
|
-
puts "An error occured while generating sound for '#{
|
103
|
-
puts " #{detail}"
|
104
|
-
puts ""
|
105
|
-
end
|
66
|
+
puts "\n"
|
67
|
+
puts "An error occured while generating sound for '#{input_file_name}':\n"
|
68
|
+
puts " #{detail}\n"
|
69
|
+
puts "\n"
|
70
|
+
end
|
data/lib/beats.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
class Beats
|
2
|
+
BEATS_VERSION = "1.2.0"
|
3
|
+
SAMPLE_RATE = 44100
|
4
|
+
OPTIMIZED_PATTERN_LENGTH = 4
|
5
|
+
|
6
|
+
def initialize(input_file_name, output_file_name, options)
|
7
|
+
@input_file_name = input_file_name
|
8
|
+
@output_file_name = output_file_name
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
if @input_file_name == nil
|
14
|
+
ARGV[0] = '-h'
|
15
|
+
parse_options()
|
16
|
+
end
|
17
|
+
|
18
|
+
if @output_file_name == nil
|
19
|
+
@output_file_name = File.basename(@input_file_name, File.extname(@input_file_name)) + ".wav"
|
20
|
+
end
|
21
|
+
|
22
|
+
song_parser = SongParser.new()
|
23
|
+
song = song_parser.parse(File.dirname(@input_file_name), YAML.load_file(@input_file_name))
|
24
|
+
song_optimizer = SongOptimizer.new()
|
25
|
+
|
26
|
+
if @options[:pattern] != nil
|
27
|
+
pattern_name = @options[:pattern].downcase.to_sym
|
28
|
+
unless song.patterns.member?(pattern_name)
|
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()
|
34
|
+
end
|
35
|
+
|
36
|
+
duration = nil
|
37
|
+
if @options[:split]
|
38
|
+
song_splitter = SongSplitter.new()
|
39
|
+
split_songs = song_splitter.split(song)
|
40
|
+
split_songs.each do |track_name, split_song|
|
41
|
+
split_song = song_optimizer.optimize(split_song, OPTIMIZED_PATTERN_LENGTH)
|
42
|
+
|
43
|
+
# TODO: Move building the output file name into its own method?
|
44
|
+
extension = File.extname(@output_file_name)
|
45
|
+
file_name = File.dirname(@output_file_name) + "/" +
|
46
|
+
File.basename(@output_file_name, extension) + "-" + File.basename(track_name, extension) +
|
47
|
+
extension
|
48
|
+
duration = split_song.write_to_file(file_name)
|
49
|
+
end
|
50
|
+
else
|
51
|
+
song = song_optimizer.optimize(song, OPTIMIZED_PATTERN_LENGTH)
|
52
|
+
duration = song.write_to_file(@output_file_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
return {:duration => duration}
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# Adds some functionality to the WaveFile gem that allows for improved performance. The
|
2
|
+
# combo of open_for_appending() and write_snippet() allow a wave file to be written to
|
3
|
+
# disk in chunks, instead of all at once. This improves performance (and I would assume
|
4
|
+
# memory usage) by eliminating the need to store the entire sample data for the song in
|
5
|
+
# memory in a giant (i.e. millions of elements) array.
|
6
|
+
#
|
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
|
+
# If I figure out a better API I might add it to the WaveFile gem in the future, but until
|
14
|
+
# then I'm just putting it here. Since BEATS is a stand-alone app and not a re-usable library,
|
15
|
+
# I don't think this should be a problem.
|
16
|
+
class BeatsWaveFile < WaveFile
|
17
|
+
|
18
|
+
# Writes the header for the wave file to path, and returns an open File object that
|
19
|
+
# 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 must be
|
21
|
+
# subsequently be written to the file using write_snippet() or it won't be valid and you
|
22
|
+
# won't be able to play it.
|
23
|
+
def open_for_appending(path, num_samples)
|
24
|
+
bytes_per_sample = (@bits_per_sample / 8)
|
25
|
+
sample_data_size = num_samples * bytes_per_sample * @num_channels
|
26
|
+
|
27
|
+
# Write the header
|
28
|
+
header = CHUNK_ID
|
29
|
+
header += [HEADER_SIZE + sample_data_size].pack("V")
|
30
|
+
header += FORMAT
|
31
|
+
header += FORMAT_CHUNK_ID
|
32
|
+
header += [SUB_CHUNK1_SIZE].pack("V")
|
33
|
+
header += [PCM].pack("v")
|
34
|
+
header += [@num_channels].pack("v")
|
35
|
+
header += [@sample_rate].pack("V")
|
36
|
+
header += [@byte_rate].pack("V")
|
37
|
+
header += [@block_align].pack("v")
|
38
|
+
header += [@bits_per_sample].pack("v")
|
39
|
+
header += DATA_CHUNK_ID
|
40
|
+
header += [sample_data_size].pack("V")
|
41
|
+
|
42
|
+
file = File.open(path, "w")
|
43
|
+
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
|
+
end
|
66
|
+
|
67
|
+
def calculate_duration(sample_rate, total_samples)
|
68
|
+
samples_per_millisecond = sample_rate / 1000.0
|
69
|
+
samples_per_second = sample_rate
|
70
|
+
samples_per_minute = samples_per_second * 60
|
71
|
+
samples_per_hour = samples_per_minute * 60
|
72
|
+
hours, minutes, seconds, milliseconds = 0, 0, 0, 0
|
73
|
+
|
74
|
+
if total_samples >= samples_per_hour
|
75
|
+
hours = total_samples / samples_per_hour
|
76
|
+
total_samples -= samples_per_hour * hours
|
77
|
+
end
|
78
|
+
|
79
|
+
if total_samples >= samples_per_minute
|
80
|
+
minutes = total_samples / samples_per_minute
|
81
|
+
total_samples -= samples_per_minute * minutes
|
82
|
+
end
|
83
|
+
|
84
|
+
if total_samples >= samples_per_second
|
85
|
+
seconds = total_samples / samples_per_second
|
86
|
+
total_samples -= samples_per_second * seconds
|
87
|
+
end
|
88
|
+
|
89
|
+
milliseconds = (total_samples / samples_per_millisecond).floor
|
90
|
+
|
91
|
+
return { :hours => hours, :minutes => minutes, :seconds => seconds, :milliseconds => milliseconds }
|
92
|
+
end
|
93
|
+
end
|
data/lib/kit.rb
CHANGED
@@ -1,38 +1,74 @@
|
|
1
|
+
# Raised when trying to load a sound file which can't be found at the path specified
|
1
2
|
class SoundNotFoundError < RuntimeError; end
|
2
3
|
|
4
|
+
# This class keeps track of the sounds that are used in a song. It provides a
|
5
|
+
# central place for storing sound data, and most usefully, handles converting
|
6
|
+
# sounds in different formats to a standard format.
|
7
|
+
#
|
8
|
+
# For example, if a song requires a sound that is mono/8-bit, another that is
|
9
|
+
# stereo/8-bit, and another that is stereo/16-bit, it can't mix them together
|
10
|
+
# because they are in different formats. Kit however automatically handles the
|
11
|
+
# details of converting them to a common format. If you add sounds to the kit
|
12
|
+
# using add(), sounds that you get using get_sample_data() will be in a common
|
13
|
+
# format.
|
14
|
+
#
|
15
|
+
# All sounds returned by get_sample_data() will be 16-bit. All sounds will be
|
16
|
+
# either mono or stereo; if at least one added sound is stereo then all sounds
|
17
|
+
# will be stereo. So for example if a mono/8-bit, stereo/8-bit, and stereo/16-bit
|
18
|
+
# sound are added, when you retrieve each one using get_sample_data() they will
|
19
|
+
# be stereo/16-bit.
|
20
|
+
#
|
21
|
+
# Note that this means that each time a new sound is added to the Kit, the common
|
22
|
+
# format might change, if the incoming sound has a greater number of channels than
|
23
|
+
# any of the previously added sounds. Therefore, all of the sounds
|
24
|
+
# used by a Song should be added to the Kit before generation begins. If you
|
25
|
+
# create Song objects by using SongParser, this will be taken care of for you (as
|
26
|
+
# long as you don't modify the Kit afterward).
|
3
27
|
class Kit
|
4
28
|
PATH_SEPARATOR = File.const_get("SEPARATOR")
|
5
29
|
|
30
|
+
# Creates a new Kit object. base_path indicates the folder from which sound files
|
31
|
+
# with relative file paths will be loaded from.
|
6
32
|
def initialize(base_path)
|
7
33
|
@base_path = base_path
|
34
|
+
@label_mappings = {}
|
8
35
|
@sounds = {}
|
9
|
-
@num_channels =
|
10
|
-
@bits_per_sample =
|
36
|
+
@num_channels = 1
|
37
|
+
@bits_per_sample = 16 # Only use 16-bit files as output. Supporting 8-bit output
|
38
|
+
# means extra complication for no real gain (I'm skeptical
|
39
|
+
# anyone would explicitly want 8-bit output instead of 16-bit.
|
11
40
|
end
|
12
41
|
|
42
|
+
# Adds a new sound to the kit.
|
13
43
|
def add(name, path)
|
14
|
-
|
15
|
-
|
16
|
-
|
44
|
+
unless @sounds.has_key? name
|
45
|
+
path_is_absolute = path.start_with?(PATH_SEPARATOR)
|
46
|
+
if path_is_absolute
|
47
|
+
full_path = path
|
48
|
+
else
|
49
|
+
full_path = @base_path + PATH_SEPARATOR + path
|
17
50
|
end
|
18
51
|
|
19
52
|
begin
|
20
|
-
wavefile = WaveFile.open(
|
53
|
+
wavefile = WaveFile.open(full_path)
|
21
54
|
rescue
|
22
|
-
|
55
|
+
# TODO: Raise different error if sound is in an unsupported format
|
56
|
+
raise SoundNotFoundError, "Sound file #{full_path} not found."
|
23
57
|
end
|
24
58
|
|
25
59
|
@sounds[name] = wavefile
|
60
|
+
if name != path
|
61
|
+
@label_mappings[name] = path
|
62
|
+
end
|
26
63
|
|
27
64
|
if wavefile.num_channels > @num_channels
|
28
65
|
@num_channels = wavefile.num_channels
|
29
66
|
end
|
30
|
-
if wavefile.bits_per_sample > @bits_per_sample
|
31
|
-
@bits_per_sample = wavefile.bits_per_sample
|
32
|
-
end
|
33
67
|
end
|
34
68
|
end
|
35
69
|
|
70
|
+
# Returns the sample data (as an Array) for a sound contained in the Kit.
|
71
|
+
# Raises an error if the sound doesn't exist in the Kit.
|
36
72
|
def get_sample_data(name)
|
37
73
|
wavefile = @sounds[name]
|
38
74
|
|
@@ -46,9 +82,29 @@ class Kit
|
|
46
82
|
end
|
47
83
|
end
|
48
84
|
|
85
|
+
# Returns the number of sounds currently contained in the kit.
|
49
86
|
def size
|
50
87
|
return @sounds.length
|
51
88
|
end
|
52
89
|
|
53
|
-
|
90
|
+
# Produces nicer looking output than the default version of to_yaml().
|
91
|
+
def to_yaml(indent_space_count = 0)
|
92
|
+
yaml = ""
|
93
|
+
longest_label_mapping_length =
|
94
|
+
@label_mappings.keys.inject(0) do |max_length, name|
|
95
|
+
(name.to_s.length > max_length) ? name.to_s.length : max_length
|
96
|
+
end
|
97
|
+
|
98
|
+
if @label_mappings.length > 0
|
99
|
+
yaml += " " * indent_space_count + "Kit:\n"
|
100
|
+
ljust_amount = longest_label_mapping_length + 1 # The +1 is for the trailing ":"
|
101
|
+
@label_mappings.sort.each do |label, path|
|
102
|
+
yaml += " " * indent_space_count + " - #{(label + ":").ljust(ljust_amount)} #{path}\n"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
return yaml
|
107
|
+
end
|
108
|
+
|
109
|
+
attr_reader :base_path, :label_mappings, :bits_per_sample, :num_channels
|
54
110
|
end
|
data/lib/pattern.rb
CHANGED
@@ -1,133 +1,178 @@
|
|
1
1
|
class Pattern
|
2
2
|
def initialize(name)
|
3
3
|
@name = name
|
4
|
-
@cache = {}
|
5
4
|
@tracks = {}
|
6
5
|
end
|
7
6
|
|
8
|
-
|
9
|
-
|
7
|
+
# Adds a new track to the pattern.
|
8
|
+
def track(name, wave_data, rhythm)
|
9
|
+
new_track = Track.new(name, wave_data, rhythm)
|
10
10
|
@tracks[new_track.name] = new_track
|
11
11
|
|
12
12
|
# If the new track is longer than any of the previously added tracks,
|
13
13
|
# pad the other tracks with trailing . to make them all the same length.
|
14
14
|
# Necessary to prevent incorrect overflow calculations for tracks.
|
15
|
-
longest_track_length =
|
16
|
-
@tracks.values.each
|
17
|
-
if
|
18
|
-
longest_track_length
|
15
|
+
longest_track_length = tick_count()
|
16
|
+
@tracks.values.each do |track|
|
17
|
+
if track.rhythm.length < longest_track_length
|
18
|
+
track.rhythm += "." * (longest_track_length - track.rhythm.length)
|
19
19
|
end
|
20
|
-
|
21
|
-
@tracks.values.each {|track|
|
22
|
-
if(track.pattern.length < longest_track_length)
|
23
|
-
track.pattern += "." * (longest_track_length - track.pattern.length)
|
24
|
-
end
|
25
|
-
}
|
20
|
+
end
|
26
21
|
|
27
22
|
return new_track
|
28
23
|
end
|
29
24
|
|
25
|
+
# The number of samples required for the pattern at the given tempo. DOES NOT include samples
|
26
|
+
# necessary for sound that overflows past the last tick of the pattern.
|
30
27
|
def sample_length(tick_sample_length)
|
31
28
|
@tracks.keys.collect {|track_name| @tracks[track_name].sample_length(tick_sample_length) }.max || 0
|
32
29
|
end
|
33
30
|
|
31
|
+
# The number of samples required for the pattern at the given tempo. Include sound overflow
|
32
|
+
# past the last tick of the pattern.
|
34
33
|
def sample_length_with_overflow(tick_sample_length)
|
35
34
|
@tracks.keys.collect {|track_name| @tracks[track_name].sample_length_with_overflow(tick_sample_length) }.max || 0
|
36
35
|
end
|
37
36
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
37
|
+
def tick_count
|
38
|
+
return @tracks.values.collect {|track| track.rhythm.length }.max || 0
|
39
|
+
end
|
40
|
+
|
41
|
+
def sample_data(tick_sample_length, num_channels, num_tracks_in_song, incoming_overflow)
|
42
|
+
primary_sample_data, overflow_sample_data = generate_main_sample_data(tick_sample_length, num_channels)
|
43
|
+
primary_sample_data, overflow_sample_data = handle_incoming_overflow(tick_sample_length,
|
44
|
+
num_channels,
|
45
|
+
incoming_overflow,
|
46
|
+
primary_sample_data,
|
47
|
+
overflow_sample_data)
|
48
|
+
primary_sample_data = mixdown_sample_data(num_channels, num_tracks_in_song, primary_sample_data)
|
49
|
+
|
50
|
+
return {:primary => primary_sample_data, :overflow => overflow_sample_data}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns whether or not this pattern has the same number of tracks as other_pattern, and that
|
54
|
+
# each of the tracks has the same name and rhythm. Ordering of tracks does not matter; will
|
55
|
+
# return true if the two patterns have the same tracks but in a different ordering.
|
56
|
+
def same_tracks_as?(other_pattern)
|
57
|
+
@tracks.keys.each do |track_name|
|
58
|
+
other_pattern_track = other_pattern.tracks[track_name]
|
59
|
+
if other_pattern_track == nil || @tracks[track_name].rhythm != other_pattern_track.rhythm
|
60
|
+
return false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
return @tracks.length == other_pattern.tracks.length
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns a YAML representation of the Pattern. Produces nicer looking output than the default
|
68
|
+
# version of to_yaml().
|
69
|
+
def to_yaml
|
70
|
+
longest_track_name_length =
|
71
|
+
@tracks.keys.inject(0) do |max_length, name|
|
72
|
+
(name.to_s.length > max_length) ? name.to_s.length : max_length
|
73
|
+
end
|
74
|
+
ljust_amount = longest_track_name_length + 7
|
75
|
+
|
76
|
+
yaml = "#{@name.to_s.capitalize}:\n"
|
77
|
+
@tracks.keys.sort.each do |track_name|
|
78
|
+
yaml += " - #{track_name}:".ljust(ljust_amount)
|
79
|
+
yaml += "#{@tracks[track_name].rhythm}\n"
|
43
80
|
end
|
81
|
+
|
82
|
+
return yaml
|
44
83
|
end
|
45
84
|
|
46
85
|
attr_accessor :tracks, :name
|
47
|
-
|
86
|
+
|
48
87
|
private
|
49
88
|
|
50
|
-
def
|
51
|
-
# If we've already encountered this pattern with the same incoming overflow before,
|
52
|
-
# return the pre-mixed down version from the cache.
|
53
|
-
if(@cache.member?(incoming_overflow))
|
54
|
-
return @cache[incoming_overflow]
|
55
|
-
end
|
56
|
-
|
57
|
-
fill_value = (num_channels == 1) ? 0 : [].fill(0, 0, num_channels)
|
89
|
+
def generate_main_sample_data(tick_sample_length, num_channels)
|
58
90
|
track_names = @tracks.keys
|
59
91
|
primary_sample_data = []
|
60
92
|
overflow_sample_data = {}
|
61
93
|
actual_sample_length = sample_length(tick_sample_length)
|
62
|
-
|
63
|
-
if
|
64
|
-
|
65
|
-
|
66
|
-
track_names.each {|track_name|
|
67
|
-
temp = @tracks[track_name].sample_data(tick_sample_length, incoming_overflow[track_name])
|
68
|
-
|
69
|
-
track_samples = temp[:primary]
|
70
|
-
if(num_channels == 1)
|
71
|
-
(0...track_samples.length).each {|i| primary_sample_data[i] += track_samples[i] }
|
72
|
-
else
|
73
|
-
(0...track_samples.length).each {|i|
|
74
|
-
primary_sample_data[i][0] += track_samples[i][0]
|
75
|
-
primary_sample_data[i][1] += track_samples[i][1]
|
76
|
-
}
|
77
|
-
end
|
78
|
-
|
79
|
-
overflow_sample_data[track_name] = temp[:overflow]
|
80
|
-
}
|
81
|
-
end
|
94
|
+
|
95
|
+
if @intermediate_cache == nil
|
96
|
+
track_names.each do |track_name|
|
97
|
+
temp = @tracks[track_name].sample_data(tick_sample_length)
|
82
98
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
if(!track_names.member?(track_name) && incoming_overflow[track_name].length > 0)
|
87
|
-
if(num_channels == 1)
|
88
|
-
(0...incoming_overflow[track_name].length).each {|i| primary_sample_data[i] += incoming_overflow[track_name][i]}
|
99
|
+
if primary_sample_data == []
|
100
|
+
primary_sample_data = temp[:primary]
|
101
|
+
overflow_sample_data[track_name] = temp[:overflow]
|
89
102
|
else
|
90
|
-
|
91
|
-
|
103
|
+
track_samples = temp[:primary]
|
104
|
+
if num_channels == 1
|
105
|
+
track_samples.length.times {|i| primary_sample_data[i] += track_samples[i] }
|
106
|
+
else
|
107
|
+
track_samples.length.times do |i|
|
108
|
+
primary_sample_data[i] = [primary_sample_data[i][0] + track_samples[i][0],
|
109
|
+
primary_sample_data[i][1] + track_samples[i][1]]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
overflow_sample_data[track_name] = temp[:overflow]
|
92
114
|
end
|
93
115
|
end
|
94
|
-
}
|
95
116
|
|
96
|
-
|
97
|
-
if(num_channels == 1)
|
98
|
-
primary_sample_data = primary_sample_data.map {|sample| (sample / num_tracks_in_song).round }
|
117
|
+
@intermediate_cache = {:primary => primary_sample_data.dup, :overflow => overflow_sample_data.dup}
|
99
118
|
else
|
100
|
-
primary_sample_data =
|
119
|
+
primary_sample_data = @intermediate_cache[:primary].dup
|
120
|
+
overflow_sample_data = @intermediate_cache[:overflow].dup
|
101
121
|
end
|
102
|
-
|
103
|
-
|
104
|
-
mixdown_sample_data = {:primary => primary_sample_data, :overflow => overflow_sample_data}
|
105
|
-
@cache[incoming_overflow] = mixdown_sample_data
|
106
|
-
|
107
|
-
return mixdown_sample_data
|
122
|
+
|
123
|
+
return primary_sample_data, overflow_sample_data
|
108
124
|
end
|
109
125
|
|
110
|
-
def
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
temp = @tracks[track_name].sample_data(tick_sample_length, incoming_overflow[track_name])
|
117
|
-
primary_sample_data[track_name] = temp[:primary]
|
118
|
-
overflow_sample_data[track_name] = temp[:overflow]
|
119
|
-
}
|
126
|
+
def handle_incoming_overflow(tick_sample_length, num_channels, incoming_overflow, primary_sample_data, overflow_sample_data)
|
127
|
+
track_names = @tracks.keys
|
128
|
+
|
129
|
+
# Add overflow from previous pattern
|
130
|
+
incoming_overflow.keys.each do |track_name|
|
131
|
+
num_incoming_overflow_samples = incoming_overflow[track_name].length
|
120
132
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
133
|
+
if num_incoming_overflow_samples > 0
|
134
|
+
if track_names.member?(track_name)
|
135
|
+
# TODO: Does this handle situations where track has a .... rhythm and overflow is
|
136
|
+
# longer than track length?
|
137
|
+
|
138
|
+
intro_length = @tracks[track_name].intro_sample_length(tick_sample_length)
|
139
|
+
if num_incoming_overflow_samples > intro_length
|
140
|
+
num_incoming_overflow_samples = intro_length
|
141
|
+
end
|
142
|
+
else
|
143
|
+
# If incoming overflow for track is longer than the pattern length, only add the first part of
|
144
|
+
# the overflow to the pattern, and add the remainder to overflow_sample_data so that it gets
|
145
|
+
# handled by the next pattern to be generated.
|
146
|
+
if num_incoming_overflow_samples > primary_sample_data.length
|
147
|
+
overflow_sample_data[track_name] = (incoming_overflow[track_name])[primary_sample_data.length...num_incoming_overflow_samples]
|
148
|
+
num_incoming_overflow_samples = primary_sample_data.length
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
if num_channels == 1
|
153
|
+
num_incoming_overflow_samples.times {|i| primary_sample_data[i] += incoming_overflow[track_name][i]}
|
154
|
+
else
|
155
|
+
num_incoming_overflow_samples.times do |i|
|
156
|
+
primary_sample_data[i] = [primary_sample_data[i][0] + incoming_overflow[track_name][i][0],
|
157
|
+
primary_sample_data[i][1] + incoming_overflow[track_name][i][1]]
|
158
|
+
end
|
159
|
+
end
|
128
160
|
end
|
129
|
-
|
130
|
-
|
131
|
-
return
|
161
|
+
end
|
162
|
+
|
163
|
+
return primary_sample_data, overflow_sample_data
|
164
|
+
end
|
165
|
+
|
166
|
+
def mixdown_sample_data(num_channels, num_tracks_in_song, primary_sample_data)
|
167
|
+
# Mix down the pattern's tracks into one single track
|
168
|
+
if num_tracks_in_song > 1
|
169
|
+
if num_channels == 1
|
170
|
+
primary_sample_data = primary_sample_data.map {|sample| sample / num_tracks_in_song }
|
171
|
+
else
|
172
|
+
primary_sample_data = primary_sample_data.map {|sample| [sample[0] / num_tracks_in_song, sample[1] / num_tracks_in_song]}
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
return primary_sample_data
|
132
177
|
end
|
133
178
|
end
|