beats 1.2.4 → 1.2.5

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