beats 1.1.0 → 1.2.0

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.
Files changed (158) hide show
  1. data/README.markdown +29 -4
  2. data/bin/beats +36 -71
  3. data/lib/beats.rb +57 -0
  4. data/lib/beatswavefile.rb +93 -0
  5. data/lib/kit.rb +67 -11
  6. data/lib/pattern.rb +131 -86
  7. data/lib/song.rb +145 -114
  8. data/lib/songoptimizer.rb +154 -0
  9. data/lib/songparser.rb +40 -28
  10. data/lib/songsplitter.rb +38 -0
  11. data/lib/track.rb +33 -31
  12. data/lib/wavefile.rb +475 -0
  13. data/test/examples/combined.wav +0 -0
  14. data/test/examples/split-agogo_high.wav +0 -0
  15. data/test/examples/split-bass.wav +0 -0
  16. data/test/examples/split-hh_closed.wav +0 -0
  17. data/test/examples/split-snare.wav +0 -0
  18. data/test/examples/split-tom2.wav +0 -0
  19. data/test/examples/split-tom4.wav +0 -0
  20. data/test/fixtures/expected_output/example_combined_mono_16.wav +0 -0
  21. data/test/fixtures/expected_output/example_combined_mono_8.wav +0 -0
  22. data/test/fixtures/expected_output/example_combined_stereo_16.wav +0 -0
  23. data/test/fixtures/expected_output/example_combined_stereo_8.wav +0 -0
  24. data/test/fixtures/expected_output/example_split_mono_16-agogo.wav +0 -0
  25. data/test/fixtures/expected_output/example_split_mono_16-bass.wav +0 -0
  26. data/test/fixtures/expected_output/example_split_mono_16-hh_closed.wav +0 -0
  27. data/test/fixtures/expected_output/example_split_mono_16-snare.wav +0 -0
  28. data/test/fixtures/expected_output/example_split_mono_16-tom2_mono_16.wav +0 -0
  29. data/test/fixtures/expected_output/example_split_mono_16-tom4_mono_16.wav +0 -0
  30. data/test/fixtures/expected_output/example_split_mono_8-agogo.wav +0 -0
  31. data/test/fixtures/expected_output/example_split_mono_8-bass.wav +0 -0
  32. data/test/fixtures/expected_output/example_split_mono_8-hh_closed.wav +0 -0
  33. data/test/fixtures/expected_output/example_split_mono_8-snare.wav +0 -0
  34. data/test/fixtures/expected_output/example_split_mono_8-tom2_mono_8.wav +0 -0
  35. data/test/fixtures/expected_output/example_split_mono_8-tom4_mono_8.wav +0 -0
  36. data/test/fixtures/expected_output/example_split_stereo_16-agogo.wav +0 -0
  37. data/test/fixtures/expected_output/example_split_stereo_16-bass.wav +0 -0
  38. data/test/fixtures/expected_output/example_split_stereo_16-hh_closed.wav +0 -0
  39. data/test/fixtures/expected_output/example_split_stereo_16-snare.wav +0 -0
  40. data/test/fixtures/expected_output/example_split_stereo_16-tom2_stereo_16.wav +0 -0
  41. data/test/fixtures/expected_output/example_split_stereo_16-tom4_stereo_16.wav +0 -0
  42. data/test/fixtures/expected_output/example_split_stereo_8-agogo.wav +0 -0
  43. data/test/fixtures/expected_output/example_split_stereo_8-bass.wav +0 -0
  44. data/test/fixtures/expected_output/example_split_stereo_8-hh_closed.wav +0 -0
  45. data/test/fixtures/expected_output/example_split_stereo_8-snare.wav +0 -0
  46. data/test/fixtures/expected_output/example_split_stereo_8-tom2_stereo_8.wav +0 -0
  47. data/test/fixtures/expected_output/example_split_stereo_8-tom4_stereo_8.wav +0 -0
  48. data/test/fixtures/invalid/bad_repeat_count.txt +8 -0
  49. data/test/fixtures/invalid/bad_rhythm.txt +9 -0
  50. data/test/fixtures/invalid/bad_structure.txt +9 -0
  51. data/test/fixtures/invalid/bad_tempo.txt +8 -0
  52. data/test/fixtures/invalid/no_header.txt +3 -0
  53. data/test/fixtures/invalid/no_structure.txt +6 -0
  54. data/test/fixtures/invalid/pattern_with_no_tracks.txt +12 -0
  55. data/test/fixtures/invalid/sound_in_kit_not_found.txt +10 -0
  56. data/test/fixtures/invalid/sound_in_track_not_found.txt +8 -0
  57. data/test/fixtures/invalid/template.txt +31 -0
  58. data/test/fixtures/valid/example_mono_16.txt +28 -0
  59. data/test/fixtures/valid/example_mono_8.txt +28 -0
  60. data/test/fixtures/valid/example_no_kit.txt +30 -0
  61. data/test/fixtures/valid/example_stereo_16.txt +28 -0
  62. data/test/fixtures/valid/example_stereo_8.txt +28 -0
  63. data/test/fixtures/valid/example_with_empty_track.txt +10 -0
  64. data/test/fixtures/valid/example_with_kit.txt +34 -0
  65. data/test/fixtures/valid/no_tempo.txt +8 -0
  66. data/test/fixtures/valid/pattern_with_overflow.txt +9 -0
  67. data/test/fixtures/valid/repeats_not_specified.txt +10 -0
  68. data/test/fixtures/yaml/song_yaml.txt +30 -0
  69. data/test/includes.rb +11 -4
  70. data/test/integration.rb +100 -0
  71. data/test/kit_test.rb +39 -39
  72. data/test/pattern_test.rb +119 -71
  73. data/test/song_test.rb +87 -62
  74. data/test/songoptimizer_test.rb +162 -0
  75. data/test/songparser_test.rb +36 -165
  76. data/test/sounds/agogo_high_mono_16.wav +0 -0
  77. data/test/sounds/agogo_high_mono_8.wav +0 -0
  78. data/test/sounds/agogo_high_stereo_16.wav +0 -0
  79. data/test/sounds/agogo_high_stereo_8.wav +0 -0
  80. data/test/sounds/agogo_low_mono_16.wav +0 -0
  81. data/test/sounds/agogo_low_mono_8.wav +0 -0
  82. data/test/sounds/agogo_low_stereo_16.wav +0 -0
  83. data/test/sounds/agogo_low_stereo_8.wav +0 -0
  84. data/test/sounds/bass2_mono_16.wav +0 -0
  85. data/test/sounds/bass2_mono_8.wav +0 -0
  86. data/test/sounds/bass2_stereo_16.wav +0 -0
  87. data/test/sounds/bass2_stereo_8.wav +0 -0
  88. data/test/sounds/bass_mono_8.wav +0 -0
  89. data/test/sounds/bass_stereo_16.wav +0 -0
  90. data/test/sounds/bass_stereo_8.wav +0 -0
  91. data/test/sounds/clave_high_mono_16.wav +0 -0
  92. data/test/sounds/clave_high_mono_8.wav +0 -0
  93. data/test/sounds/clave_high_stereo_16.wav +0 -0
  94. data/test/sounds/clave_high_stereo_8.wav +0 -0
  95. data/test/sounds/clave_low_mono_16.wav +0 -0
  96. data/test/sounds/clave_low_mono_8.wav +0 -0
  97. data/test/sounds/clave_low_stereo_16.wav +0 -0
  98. data/test/sounds/clave_low_stereo_8.wav +0 -0
  99. data/test/sounds/conga_high_mono_16.wav +0 -0
  100. data/test/sounds/conga_high_mono_8.wav +0 -0
  101. data/test/sounds/conga_high_stereo_16.wav +0 -0
  102. data/test/sounds/conga_high_stereo_8.wav +0 -0
  103. data/test/sounds/conga_low_mono_16.wav +0 -0
  104. data/test/sounds/conga_low_mono_8.wav +0 -0
  105. data/test/sounds/conga_low_stereo_16.wav +0 -0
  106. data/test/sounds/conga_low_stereo_8.wav +0 -0
  107. data/test/sounds/cowbell_high_mono_16.wav +0 -0
  108. data/test/sounds/cowbell_high_mono_8.wav +0 -0
  109. data/test/sounds/cowbell_high_stereo_16.wav +0 -0
  110. data/test/sounds/cowbell_high_stereo_8.wav +0 -0
  111. data/test/sounds/cowbell_low_mono_16.wav +0 -0
  112. data/test/sounds/cowbell_low_mono_8.wav +0 -0
  113. data/test/sounds/cowbell_low_stereo_16.wav +0 -0
  114. data/test/sounds/cowbell_low_stereo_8.wav +0 -0
  115. data/test/sounds/hh_closed_mono_16.wav +0 -0
  116. data/test/sounds/hh_closed_mono_8.wav +0 -0
  117. data/test/sounds/hh_closed_stereo_16.wav +0 -0
  118. data/test/sounds/hh_closed_stereo_8.wav +0 -0
  119. data/test/sounds/hh_open_mono_16.wav +0 -0
  120. data/test/sounds/hh_open_mono_8.wav +0 -0
  121. data/test/sounds/hh_open_stereo_16.wav +0 -0
  122. data/test/sounds/hh_open_stereo_8.wav +0 -0
  123. data/test/sounds/ride_mono_16.wav +0 -0
  124. data/test/sounds/ride_mono_8.wav +0 -0
  125. data/test/sounds/ride_stereo_16.wav +0 -0
  126. data/test/sounds/ride_stereo_8.wav +0 -0
  127. data/test/sounds/rim_mono_16.wav +0 -0
  128. data/test/sounds/rim_mono_8.wav +0 -0
  129. data/test/sounds/rim_stereo_16.wav +0 -0
  130. data/test/sounds/rim_stereo_8.wav +0 -0
  131. data/test/sounds/sine-mono-8bit.wav +0 -0
  132. data/test/sounds/snare2_mono_16.wav +0 -0
  133. data/test/sounds/snare2_mono_8.wav +0 -0
  134. data/test/sounds/snare2_stereo_16.wav +0 -0
  135. data/test/sounds/snare2_stereo_8.wav +0 -0
  136. data/test/sounds/snare_mono_16.wav +0 -0
  137. data/test/sounds/snare_mono_8.wav +0 -0
  138. data/test/sounds/snare_stereo_16.wav +0 -0
  139. data/test/sounds/snare_stereo_8.wav +0 -0
  140. data/test/sounds/tom1_mono_16.wav +0 -0
  141. data/test/sounds/tom1_mono_8.wav +0 -0
  142. data/test/sounds/tom1_stereo_16.wav +0 -0
  143. data/test/sounds/tom1_stereo_8.wav +0 -0
  144. data/test/sounds/tom2_mono_16.wav +0 -0
  145. data/test/sounds/tom2_mono_8.wav +0 -0
  146. data/test/sounds/tom2_stereo_16.wav +0 -0
  147. data/test/sounds/tom2_stereo_8.wav +0 -0
  148. data/test/sounds/tom3_mono_16.wav +0 -0
  149. data/test/sounds/tom3_mono_8.wav +0 -0
  150. data/test/sounds/tom3_stereo_16.wav +0 -0
  151. data/test/sounds/tom3_stereo_8.wav +0 -0
  152. data/test/sounds/tom4_mono_16.wav +0 -0
  153. data/test/sounds/tom4_mono_8.wav +0 -0
  154. data/test/sounds/tom4_stereo_16.wav +0 -0
  155. data/test/sounds/tom4_stereo_8.wav +0 -0
  156. data/test/sounds/tone.wav +0 -0
  157. data/test/track_test.rb +78 -72
  158. metadata +277 -15
@@ -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 Wave file of impeccable timing and feel. Here's an example song:
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
- For installation and usage instructions, visit the BEATS website at [http://beatsdrummachine.com](http://beatsdrummachine.com).
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
- require File.dirname(__FILE__) + "/../lib/song"
4
- require File.dirname(__FILE__) + "/../lib/songparser"
5
- require File.dirname(__FILE__) + "/../lib/kit"
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 "wavefile"
12
-
13
- BEATS_VERSION = "1.1.0"
14
- SAMPLE_RATE = 44100
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
- def save_wave_file(file_name, num_channels, bits_per_sample, sample_data)
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
- input_file = ARGV[0]
54
- output_file = ARGV[1]
47
+ input_file_name = ARGV[0]
48
+ output_file_name = ARGV[1]
55
49
 
56
- if(input_file == nil)
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
- parse_start_time = Time.now
67
- song_parser = SongParser.new()
68
- song_from_file = song_parser.parse(File.dirname(input_file), YAML.load_file(input_file))
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 '#{input_file}' not found."
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 '#{input_file}' has an error:"
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 '#{input_file}':"
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
@@ -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 = 0
10
- @bits_per_sample = 0
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
- if(!@sounds.has_key? name)
15
- if(!path.start_with?(PATH_SEPARATOR))
16
- path = @base_path + PATH_SEPARATOR + path
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(path)
53
+ wavefile = WaveFile.open(full_path)
21
54
  rescue
22
- raise SoundNotFoundError, "Sound file #{name} not found."
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
- attr_reader :base_path, :bits_per_sample, :num_channels
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
@@ -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
- def track(name, wave_data, pattern)
9
- new_track = Track.new(name, wave_data, pattern)
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 = 0
16
- @tracks.values.each {|track|
17
- if(track.pattern.length > longest_track_length)
18
- longest_track_length = track.pattern.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 sample_data(tick_sample_length, num_channels, num_tracks_in_song, incoming_overflow, split = false)
39
- if(split)
40
- return split_sample_data(tick_sample_length, num_channels, incoming_overflow)
41
- else
42
- return combined_sample_data(tick_sample_length, num_channels, num_tracks_in_song, incoming_overflow)
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 combined_sample_data(tick_sample_length, num_channels, num_tracks_in_song, incoming_overflow)
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(track_names.length > 0)
64
- primary_sample_data = [].fill(fill_value, 0, actual_sample_length)
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
- # Add samples for tracks with overflow from previous pattern, but not
84
- # contained in current pattern.
85
- incoming_overflow.keys.each {|track_name|
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
- (0...incoming_overflow[track_name].length).each {|i| primary_sample_data[i][0] += incoming_overflow[track_name][i][0]
91
- primary_sample_data[i][1] += incoming_overflow[track_name][i][1]}
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
- # Mix down the pattern's tracks into one single track
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 = primary_sample_data.map {|sample| [(sample[0] / num_tracks_in_song).round, (sample[1] / num_tracks_in_song).round] }
119
+ primary_sample_data = @intermediate_cache[:primary].dup
120
+ overflow_sample_data = @intermediate_cache[:overflow].dup
101
121
  end
102
-
103
- # Add the result to the cache so we don't have to go through all of this the next time...
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 split_sample_data(tick_sample_length, num_channels, incoming_overflow)
111
- fill_value = (num_channels == 1) ? 0 : [].fill(0, 0, num_channels)
112
- primary_sample_data = {}
113
- overflow_sample_data = {}
114
-
115
- @tracks.keys.each {|track_name|
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
- incoming_overflow.keys.each {|track_name|
122
- if(@tracks[track_name] == nil)
123
- # TO DO: Add check for when incoming overflow is longer than
124
- # track full length to prevent track from lengthening.
125
- primary_sample_data[track_name] = [].fill(fill_value, 0, sample_length(tick_sample_length))
126
- primary_sample_data[track_name][0...incoming_overflow[track_name].length] = incoming_overflow[track_name]
127
- overflow_sample_data[track_name] = []
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 {:primary => primary_sample_data, :overflow => overflow_sample_data}
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