beats 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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