beats 1.2.4 → 1.2.5

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4f2f7351421971cb29e95868abde34acaa5ab6b2
4
+ data.tar.gz: ba692d9e94e5cac568eff994493a45b4739ee2ac
5
+ SHA512:
6
+ metadata.gz: 23dd31bfc7e19dcf52dcc573e054f36b46f93b4cd5d5aaa9e9077d9a9227082ddf42b4781f9564af1ab54fd70de1758116e8cf94c6189ca1bbf3f7426628b8dc
7
+ data.tar.gz: 19d77737f3a74672d0d73267ba673ecebf99c46c8af487d75762ee8956aab9e034d888d39707e1d6ef10cb6bdb3fd42f499b838b6e032290db664a2476ec5a68
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  == BEATS
2
2
 
3
- # Copyright (c) 2010-12 Joel Strait
3
+ # Copyright (c) 2010-13 Joel Strait
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person
6
6
  # obtaining a copy of this software and associated documentation
@@ -33,16 +33,19 @@ And [here's what it sounds like](http://beatsdrummachine.com/media/beat.mp3) aft
33
33
  Current Status
34
34
  --------------
35
35
 
36
- The latest stable version of Beats is 1.2.4, released on December 22, 2012. This is a minor release which includes two improvements:
36
+ The latest stable version of Beats is 1.2.5, released on December 31, 2013. This release includes these improvements:
37
37
 
38
- * Now works in MRI 1.9.3
39
- * Now supports 32-bit PCM Wave files, due to upgrading to WaveFile 0.4.0. Previously, only 8-bit and 16-bit PCM files were supported.
38
+ * Tracks that start with a `|` no longer cause an error in Ruby 2.0.0 and 2.1.0.
39
+ * Additional Wave file formats can now be used as samples, due to upgrading to [WaveFile 0.6.0](http://wavefilegem.com) behind the scenes:
40
+ * 24-bit PCM
41
+ * 32-bit IEEE Float
42
+ * 64-bit IEEE Float
40
43
 
41
44
 
42
45
  Installation
43
46
  ------------
44
47
 
45
- To install the latest stable version (1.2.4) from [rubygems.org](http://rubygems.org/gems/beats), run the following from the command line:
48
+ To install the latest stable version (1.2.5) from [rubygems.org](http://rubygems.org/gems/beats), run the following from the command line:
46
49
 
47
50
  gem install beats
48
51
 
@@ -61,6 +64,23 @@ Beats runs from the command-line. Run `beats -h` to see the available options. F
61
64
  The Beats wiki also has a [Getting Started](https://github.com/jstrait/beats/wiki/Getting-Started) tutorial which shows how to create an example beat from scratch.
62
65
 
63
66
 
67
+ Local Development
68
+ -----------------
69
+
70
+ First, install the required dependencies:
71
+
72
+ bundle install
73
+
74
+ To run Beats locally, use `bundle exec` and run `bin/beats`, to avoid using any installed gem executable. For example:
75
+
76
+ bundle exec bin/beats -v
77
+
78
+ To run the tests:
79
+
80
+ bundle exec rake test
81
+
82
+
83
+
64
84
  Found a Bug? Have a Suggestion? Want to Contribute?
65
85
  ---------------------------------------------------
66
86
 
data/Rakefile CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'rake/testtask'
2
-
2
+
3
3
  Rake::TestTask.new do |t|
4
4
  t.libs << 'lib' << 'test'
5
5
  t.pattern = 'test/**/*_test.rb'
data/bin/beats CHANGED
@@ -2,20 +2,15 @@
2
2
 
3
3
  start_time = Time.now
4
4
 
5
- $:.unshift File.dirname(__FILE__) + "/.."
5
+ $:.unshift File.dirname(__FILE__) + "/../lib"
6
6
  require "optparse"
7
7
  require "yaml"
8
+ require "syck"
8
9
  require "wavefile"
9
- require "lib/audioengine"
10
- require "lib/audioutils"
11
- require "lib/beats"
12
- require "lib/wavefile/cachingwriter"
13
- require "lib/kit"
14
- require "lib/pattern"
15
- require "lib/song"
16
- require "lib/songoptimizer"
17
- require "lib/songparser"
18
- require "lib/track"
10
+ require "beats"
11
+ require "wavefile/cachingwriter"
12
+
13
+ include Beats
19
14
 
20
15
  USAGE_INSTRUCTIONS = ""
21
16
  YAML::ENGINE.yamler = 'syck' if defined?(YAML::ENGINE)
@@ -39,7 +34,7 @@ def parse_options()
39
34
  end
40
35
 
41
36
  opts.on('-v', '--version', "Display version number and exit") do
42
- puts "BEATS v#{Beats::BEATS_VERSION}"
37
+ puts "Beats Drum Machine #{Beats::VERSION}"
43
38
  exit
44
39
  end
45
40
 
@@ -69,7 +64,7 @@ def print_error(error, input_file_name)
69
64
  puts "An error occured while generating sound for '#{input_file_name}':\n"
70
65
  puts " #{error}\n"
71
66
  else
72
- puts "An unexpected error occured while running BEATS:"
67
+ puts "An unexpected error occured while running Beats Drum Machine:"
73
68
  puts " #{error}\n"
74
69
  end
75
70
  end
@@ -79,11 +74,11 @@ begin
79
74
  input_file_name = ARGV[0]
80
75
  output_file_name = ARGV[1]
81
76
 
82
- beats = Beats.new(input_file_name, output_file_name, options)
77
+ beats = BeatsRunner.new(input_file_name, output_file_name, options)
83
78
 
84
79
  output = beats.run()
85
80
  duration = output[:duration]
86
- puts "#{duration[:minutes]}:#{duration[:seconds].to_s.rjust(2, '0')} of audio written in #{Time.now - start_time} seconds."
81
+ puts "#{duration.minutes}:#{duration.seconds.to_s.rjust(2, '0')} of audio written in #{Time.now - start_time} seconds."
87
82
  rescue => error
88
83
  print_error(error, input_file_name)
89
84
  exit(false)
@@ -0,0 +1,28 @@
1
+ # Based on: http://www.programmersparadox.com/2012/05/21/gemspec-loading-dependent-gems-based-on-the-users-system/
2
+
3
+ # This file needs to be named mkrf_conf.rb
4
+ # so that rubygems will recognize it as a ruby extension
5
+ # file and not think it is a C extension file
6
+
7
+ require 'rubygems/dependency_installer.rb'
8
+
9
+ # Load up the rubygem's dependency installer to
10
+ # installer the gems we want based on the version
11
+ # of Ruby the user has installed
12
+ installer = Gem::DependencyInstaller.new
13
+ begin
14
+ if RUBY_VERSION >= "2"
15
+ installer.install "syck"
16
+ end
17
+
18
+ rescue
19
+ # Exit with a non-zero value to let rubygems something went wrong
20
+ exit(1)
21
+ end
22
+
23
+ # If this was C, rubygems would attempt to run make
24
+ # Since this is Ruby, rubygems will attempt to run rake
25
+ # If it doesn't find and successfully run a rakefile, it errors out
26
+ f = File.open(File.join(File.dirname(__FILE__), "Rakefile"), "w")
27
+ f.write("task :default\n")
28
+ f.close
@@ -1,77 +1,13 @@
1
- class Beats
2
- BEATS_VERSION = "1.2.4"
3
-
4
- # Each pattern in the song will be split up into sub patterns that have at most this many steps.
5
- # In general, audio for several shorter patterns can be generated more quickly than for one long
6
- # pattern, and can also be cached more effectively.
7
- OPTIMIZED_PATTERN_LENGTH = 4
8
-
9
- def initialize(input_file_name, output_file_name, options)
10
- @input_file_name = input_file_name
11
-
12
- if output_file_name == nil
13
- output_file_name = File.basename(input_file_name, File.extname(input_file_name)) + ".wav"
14
- end
15
- @output_file_name = output_file_name
16
-
17
- @options = options
18
- end
19
-
20
- def run
21
- base_path = @options[:base_path] || File.dirname(@input_file_name)
22
- song, kit = SongParser.new().parse(base_path, File.read(@input_file_name))
23
-
24
- song = normalize_for_pattern_option(song)
25
- songs_to_generate = normalize_for_split_option(song)
26
-
27
- duration = nil
28
- song_optimizer = SongOptimizer.new()
29
- songs_to_generate.each do |output_file_name, song|
30
- song = song_optimizer.optimize(song, OPTIMIZED_PATTERN_LENGTH)
31
- duration = AudioEngine.new(song, kit).write_to_file(output_file_name)
32
- end
33
-
34
- return {:duration => duration}
35
- end
36
-
37
- private
38
-
39
- # If the -p option is used, transform the song into one whose flow consists of
40
- # playing that single pattern once.
41
- def normalize_for_pattern_option(song)
42
- unless @options[:pattern] == nil
43
- pattern_name = @options[:pattern].downcase.to_sym
44
-
45
- unless song.patterns.has_key?(pattern_name)
46
- raise StandardError, "The song does not include a pattern called #{pattern_name}"
47
- end
48
-
49
- song.flow = [pattern_name]
50
- song.remove_unused_patterns()
51
- end
52
-
53
- return song
54
- end
55
-
56
- # Returns a hash of file name => song object for each song that should go through the audio engine
57
- def normalize_for_split_option(song)
58
- songs_to_generate = {}
59
-
60
- if @options[:split]
61
- split_songs = song.split()
62
- split_songs.each do |track_name, split_song|
63
- # TODO: Move building the output file name into its own method?
64
- extension = File.extname(@output_file_name)
65
- file_name = File.dirname(@output_file_name) + "/" +
66
- File.basename(@output_file_name, extension) + "-" + File.basename(track_name, extension) +
67
- extension
68
-
69
- songs_to_generate[file_name] = split_song
70
- end
71
- else
72
- songs_to_generate[@output_file_name] = song
73
- end
74
-
75
- return songs_to_generate
76
- end
1
+ require 'beats/audioengine'
2
+ require 'beats/audioutils'
3
+ require 'beats/beatsrunner'
4
+ require 'beats/kit'
5
+ require 'beats/pattern'
6
+ require 'beats/song'
7
+ require 'beats/songparser'
8
+ require 'beats/songoptimizer'
9
+ require 'beats/track'
10
+
11
+ module Beats
12
+ VERSION = "1.2.5"
77
13
  end
@@ -0,0 +1,163 @@
1
+ module Beats
2
+ # This class actually generates the output audio data that is saved to disk.
3
+ #
4
+ # To produce audio data, it needs two things: a Song and a Kit. The Song tells
5
+ # it which sounds to trigger and when, while the Kit provides the sample data
6
+ # for each of these sounds.
7
+ #
8
+ # Example usage, assuming song and kit are already defined:
9
+ #
10
+ # engine = AudioEngine.new(song, kit)
11
+ # engine.write_to_file("my_song.wav")
12
+ #
13
+ class AudioEngine
14
+ SAMPLE_RATE = 44100
15
+ PACK_CODE = "s*" # All output sample data is assumed to be 16-bit
16
+
17
+ def initialize(song, kit)
18
+ @song = song
19
+ @kit = kit
20
+
21
+ @step_sample_length = AudioUtils.step_sample_length(SAMPLE_RATE, @song.tempo)
22
+ @composited_pattern_cache = {}
23
+ end
24
+
25
+ def write_to_file(output_file_name)
26
+ packed_pattern_cache = {}
27
+ num_tracks_in_song = @song.total_tracks
28
+
29
+ # Open output wave file and prepare it for writing sample data.
30
+ format = WaveFile::Format.new(@kit.num_channels, @kit.bits_per_sample, SAMPLE_RATE)
31
+ writer = WaveFile::CachingWriter.new(output_file_name, format)
32
+
33
+ # Generate each pattern's sample data, or pull it from cache, and append it to the wave file.
34
+ incoming_overflow = {}
35
+ @song.flow.each do |pattern_name|
36
+ key = [pattern_name, incoming_overflow.hash]
37
+ unless packed_pattern_cache.member?(key)
38
+ sample_data = generate_pattern_sample_data(@song.patterns[pattern_name], incoming_overflow)
39
+
40
+ packed_pattern_cache[key] = { :primary => WaveFile::Buffer.new(sample_data[:primary], format),
41
+ :overflow => WaveFile::Buffer.new(sample_data[:overflow], format) }
42
+ end
43
+
44
+ writer.write(packed_pattern_cache[key][:primary])
45
+ incoming_overflow = packed_pattern_cache[key][:overflow].samples
46
+ end
47
+
48
+ # Write any remaining overflow from the final pattern
49
+ final_overflow_composite = AudioUtils.composite(incoming_overflow.values, format.channels)
50
+ final_overflow_composite = AudioUtils.scale(final_overflow_composite, format.channels, num_tracks_in_song)
51
+ writer.write(WaveFile::Buffer.new(final_overflow_composite, format))
52
+
53
+ writer.close()
54
+
55
+ writer.total_duration
56
+ end
57
+
58
+ attr_reader :step_sample_length
59
+
60
+ private
61
+
62
+ # Generates the sample data for a single track, using the specified sound's sample data.
63
+ def generate_track_sample_data(track, sound)
64
+ beats = track.beats
65
+ if beats == [0]
66
+ return {:primary => [], :overflow => []} # Is this really what should happen? Why throw away overflow?
67
+ end
68
+
69
+ fill_value = (@kit.num_channels == 1) ? 0 : [0, 0]
70
+ primary_sample_data = [].fill(fill_value, 0, AudioUtils.step_start_sample(track.step_count, @step_sample_length))
71
+
72
+ step_index = beats[0]
73
+ beat_sample_length = 0
74
+ beats[1...(beats.length)].each do |beat_step_length|
75
+ start_sample = AudioUtils.step_start_sample(step_index, @step_sample_length)
76
+ end_sample = [(start_sample + sound.length), primary_sample_data.length].min
77
+ beat_sample_length = end_sample - start_sample
78
+
79
+ primary_sample_data[start_sample...end_sample] = sound[0...beat_sample_length]
80
+
81
+ step_index += beat_step_length
82
+ end
83
+
84
+ overflow_sample_data = (sound == [] || beats.length == 1) ? [] : sound[beat_sample_length...(sound.length)]
85
+
86
+ {:primary => primary_sample_data, :overflow => overflow_sample_data}
87
+ end
88
+
89
+ # Composites the sample data for each of the pattern's tracks, and returns the overflow sample data
90
+ # from tracks whose last sound trigger extends past the end of the pattern. This overflow can be
91
+ # used by the next pattern to avoid sounds cutting off when the pattern changes.
92
+ def generate_pattern_sample_data(pattern, incoming_overflow)
93
+ # Unless cached, composite each track's sample data.
94
+ if @composited_pattern_cache[pattern].nil?
95
+ primary_sample_data, overflow_sample_data = composite_pattern_tracks(pattern)
96
+ @composited_pattern_cache[pattern] = {:primary => primary_sample_data.dup, :overflow => overflow_sample_data.dup}
97
+ else
98
+ primary_sample_data = @composited_pattern_cache[pattern][:primary].dup
99
+ overflow_sample_data = @composited_pattern_cache[pattern][:overflow].dup
100
+ end
101
+
102
+ # Composite overflow from the previous pattern onto this pattern, to prevent sounds from cutting off.
103
+ primary_sample_data, overflow_sample_data = handle_incoming_overflow(pattern,
104
+ incoming_overflow,
105
+ primary_sample_data,
106
+ overflow_sample_data)
107
+ primary_sample_data = AudioUtils.scale(primary_sample_data, @kit.num_channels, @song.total_tracks)
108
+
109
+ {:primary => primary_sample_data, :overflow => overflow_sample_data}
110
+ end
111
+
112
+ def composite_pattern_tracks(pattern)
113
+ overflow_sample_data = {}
114
+
115
+ raw_track_sample_arrays = []
116
+ pattern.tracks.each do |track_name, track|
117
+ temp = generate_track_sample_data(track, @kit.get_sample_data(track.name))
118
+ raw_track_sample_arrays << temp[:primary]
119
+ overflow_sample_data[track_name] = temp[:overflow]
120
+ end
121
+
122
+ primary_sample_data = AudioUtils.composite(raw_track_sample_arrays, @kit.num_channels)
123
+ return primary_sample_data, overflow_sample_data
124
+ end
125
+
126
+ # Applies sound overflow (i.e. long sounds such as cymbal crash which extend past the last step)
127
+ # from the previous pattern in the flow to the current pattern. This prevents sounds from being
128
+ # cut off when the pattern changes.
129
+ #
130
+ # It would probably be shorter and conceptually simpler to deal with incoming overflow in
131
+ # generate_track_sample_data() instead of this method. (In fact, this method would go away).
132
+ # However, doing it this way allows for caching composited pattern sample data, and
133
+ # applying incoming overflow to the composite. This allows each pattern to only be composited once,
134
+ # regardless of the incoming overflow that each performance of it receives. If incoming overflow
135
+ # was handled at the Track level we couldn't do that.
136
+ def handle_incoming_overflow(pattern, incoming_overflow, primary_sample_data, overflow_sample_data)
137
+ pattern_track_names = pattern.tracks.keys
138
+ sample_arrays = [primary_sample_data]
139
+
140
+ incoming_overflow.each do |incoming_track_name, incoming_sample_data|
141
+ end_sample = incoming_sample_data.length
142
+
143
+ if pattern_track_names.member?(incoming_track_name)
144
+ track = pattern.tracks[incoming_track_name]
145
+
146
+ if track.beats.length > 1
147
+ intro_length = (pattern.tracks[incoming_track_name].beats[0] * step_sample_length).floor
148
+ end_sample = [end_sample, intro_length].min
149
+ end
150
+ end
151
+
152
+ if end_sample > primary_sample_data.length
153
+ end_sample = primary_sample_data.length
154
+ overflow_sample_data[incoming_track_name] = incoming_sample_data[(primary_sample_data.length)...(incoming_sample_data.length)]
155
+ end
156
+
157
+ sample_arrays << incoming_sample_data[0...end_sample]
158
+ end
159
+
160
+ return AudioUtils.composite(sample_arrays, @kit.num_channels), overflow_sample_data
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,74 @@
1
+ module Beats
2
+ # This class contains some utility methods for working with sample data.
3
+ module AudioUtils
4
+
5
+ # Combines multiple sample arrays into one, by adding them together.
6
+ # When the sample arrays are different lengths, the output array will be the length
7
+ # of the longest input array.
8
+ # WARNING: Incoming arrays can be modified.
9
+ def self.composite(sample_arrays, num_channels)
10
+ if sample_arrays == []
11
+ return []
12
+ end
13
+
14
+ # Sort from longest to shortest
15
+ sample_arrays = sample_arrays.sort {|x, y| y.length <=> x.length}
16
+
17
+ composited_output = sample_arrays.slice!(0)
18
+ sample_arrays.each do |sample_array|
19
+ unless sample_array == []
20
+ if num_channels == 1
21
+ sample_array.length.times {|i| composited_output[i] += sample_array[i] }
22
+ elsif num_channels == 2
23
+ sample_array.length.times do |i|
24
+ composited_output[i] = [composited_output[i][0] + sample_array[i][0],
25
+ composited_output[i][1] + sample_array[i][1]]
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ composited_output
32
+ end
33
+
34
+
35
+ # Scales the amplitude of the incoming sample array by *scale* amount. Can be used in conjunction
36
+ # with composite() to make sure composited sample arrays don't have an amplitude greater than 1.0.
37
+ def self.scale(sample_array, num_channels, scale)
38
+ if sample_array == []
39
+ return sample_array
40
+ end
41
+
42
+ if scale > 1
43
+ if num_channels == 1
44
+ sample_array = sample_array.map {|sample| sample / scale }
45
+ elsif num_channels == 2
46
+ sample_array = sample_array.map {|sample| [sample[0] / scale, sample[1] / scale]}
47
+ else
48
+ raise StandardError, "Invalid sample data array in AudioUtils.normalize()"
49
+ end
50
+ end
51
+
52
+ sample_array
53
+ end
54
+
55
+
56
+ # Returns the number of samples that each step (i.e. a 'X' or a '.') lasts at a given sample
57
+ # rate and tempo. The sample length can be a non-integer value. Although there's no such
58
+ # thing as a partial sample, this is required to prevent small timing errors from creeping in.
59
+ # If they accumulate, they can cause rhythms to drift out of time.
60
+ def self.step_sample_length(samples_per_second, tempo)
61
+ samples_per_minute = samples_per_second * 60.0
62
+ samples_per_quarter_note = samples_per_minute / tempo
63
+
64
+ # Each step is equivalent to a 16th note
65
+ samples_per_quarter_note / 4.0
66
+ end
67
+
68
+
69
+ # Returns the sample index that a given step (offset from 0) starts on.
70
+ def self.step_start_sample(step_index, step_sample_length)
71
+ (step_index * step_sample_length).floor
72
+ end
73
+ end
74
+ end