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,76 @@
1
+ module Beats
2
+ class BeatsRunner
3
+ # Each pattern in the song will be split up into sub patterns that have at most this many steps.
4
+ # In general, audio for several shorter patterns can be generated more quickly than for one long
5
+ # pattern, and can also be cached more effectively.
6
+ OPTIMIZED_PATTERN_LENGTH = 4
7
+
8
+ def initialize(input_file_name, output_file_name, options)
9
+ @input_file_name = input_file_name
10
+
11
+ if output_file_name.nil?
12
+ output_file_name = File.basename(input_file_name, File.extname(input_file_name)) + ".wav"
13
+ end
14
+ @output_file_name = output_file_name
15
+
16
+ @options = options
17
+ end
18
+
19
+ def run
20
+ base_path = @options[:base_path] || File.dirname(@input_file_name)
21
+ song, kit = SongParser.new().parse(base_path, File.read(@input_file_name))
22
+
23
+ song = normalize_for_pattern_option(song)
24
+ songs_to_generate = normalize_for_split_option(song)
25
+
26
+ song_optimizer = SongOptimizer.new()
27
+ durations = songs_to_generate.collect do |output_file_name, song|
28
+ song = song_optimizer.optimize(song, OPTIMIZED_PATTERN_LENGTH)
29
+ AudioEngine.new(song, kit).write_to_file(output_file_name)
30
+ end
31
+
32
+ {:duration => durations.last}
33
+ end
34
+
35
+ private
36
+
37
+ # If the -p option is used, transform the song into one whose flow consists of
38
+ # playing that single pattern once.
39
+ def normalize_for_pattern_option(song)
40
+ unless @options[:pattern].nil?
41
+ pattern_name = @options[:pattern].downcase.to_sym
42
+
43
+ unless song.patterns.has_key?(pattern_name)
44
+ raise StandardError, "The song does not include a pattern called #{pattern_name}"
45
+ end
46
+
47
+ song.flow = [pattern_name]
48
+ song.remove_unused_patterns()
49
+ end
50
+
51
+ song
52
+ end
53
+
54
+ # Returns a hash of file name => song object for each song that should go through the audio engine
55
+ def normalize_for_split_option(song)
56
+ songs_to_generate = {}
57
+
58
+ if @options[:split]
59
+ split_songs = song.split()
60
+ split_songs.each do |track_name, split_song|
61
+ # TODO: Move building the output file name into its own method?
62
+ extension = File.extname(@output_file_name)
63
+ file_name = File.dirname(@output_file_name) + "/" +
64
+ File.basename(@output_file_name, extension) + "-" + File.basename(track_name, extension) +
65
+ extension
66
+
67
+ songs_to_generate[file_name] = split_song
68
+ end
69
+ else
70
+ songs_to_generate[@output_file_name] = song
71
+ end
72
+
73
+ songs_to_generate
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,187 @@
1
+ module Beats
2
+ # Raised when trying to load a sound file which can't be found at the path specified
3
+ class SoundFileNotFoundError < RuntimeError; end
4
+
5
+ # Raised when trying to load a sound file which either isn't actually a sound file, or
6
+ # is in an unsupported format.
7
+ class InvalidSoundFormatError < RuntimeError; end
8
+
9
+
10
+ # This class provides a repository for the sounds used in a song. Most usefully, it
11
+ # also handles converting the sounds to a common format. For example, if a song requires
12
+ # a sound that is mono/8-bit, another that is stereo/8-bit, and another that is
13
+ # stereo/16-bit, they have to be converted to a common format before they can be used
14
+ # together. Kit handles this conversion; all sounds retrieved using
15
+ # get_sample_data() will be in a common format.
16
+ #
17
+ # Sounds can only be added at initialization. During initialization, the sample data
18
+ # for each sound is loaded into memory, and converted to the common format if necessary.
19
+ # This format is:
20
+ #
21
+ # Bits per sample: 16
22
+ # Sample rate: 44100
23
+ # Channels: Stereo, unless all of the kit sounds are mono.
24
+ #
25
+ # For example if the kit has these sounds:
26
+ #
27
+ # my_sound_1.wav: mono, 16-bit
28
+ # my_sound_2.wav: stereo, 8-bit
29
+ # my_sound_3.wav: mono, 8-bit
30
+ #
31
+ # they will all be converted to stereo/16-bit during initialization.
32
+ class Kit
33
+ def initialize(base_path, kit_items)
34
+ @base_path = base_path
35
+ @label_mappings = {}
36
+ @sound_bank = {}
37
+ @num_channels = 1
38
+ @bits_per_sample = 16 # Only use 16-bit files as output. Supporting 8-bit output
39
+ # means extra complication for no real gain (I'm skeptical
40
+ # anyone would explicitly want 8-bit output instead of 16-bit).
41
+
42
+ load_sounds(base_path, kit_items)
43
+ end
44
+
45
+ # Returns the sample data for a sound contained in the Kit. If the all sounds in the
46
+ # kit are mono, then this will be a flat Array of Fixnums between -32768 and 32767.
47
+ # Otherwise, this will be an Array of Fixnums pairs between -32768 and 32767.
48
+ #
49
+ # label - The name of the sound to get sample data for. If the sound was defined in
50
+ # the Kit section of a song file, this will generally be a descriptive label
51
+ # such as "bass" or "snare". If defined in a track but not the kit, it will
52
+ # generally be a file name such as "my_sounds/hihat/hi_hat.wav".
53
+ #
54
+ # Examples
55
+ #
56
+ # # If @num_channels is 1, a flat Array of Fixnums:
57
+ # get_sample_data("bass")
58
+ # # => [154, 7023, 8132, 2622, -132, 34, ..., -6702]
59
+ #
60
+ # # If @num_channels is 2, a Array of Fixnums pairs:
61
+ # get_sample_data("snare")
62
+ # # => [[57, 1265], [-452, 10543], [-2531, 12643], [-6372, 11653], ..., [5482, 25673]]
63
+ #
64
+ # Returns the sample data Array for the sound bound to label.
65
+ def get_sample_data(label)
66
+ if label == "placeholder"
67
+ return []
68
+ end
69
+
70
+ sample_data = @sound_bank[label]
71
+
72
+ if sample_data.nil?
73
+ # TODO: Should we really throw an exception here rather than just returning nil?
74
+ raise StandardError, "Kit doesn't contain sound '#{label}'."
75
+ else
76
+ return sample_data
77
+ end
78
+ end
79
+
80
+ def scale!(scale_factor)
81
+ @sound_bank.each do |label, sample_array|
82
+ @sound_bank[label] = AudioUtils.scale(sample_array, @num_channels, scale_factor)
83
+ end
84
+ end
85
+
86
+ # Returns a YAML representation of the Kit. Produces nicer looking output than the default version
87
+ # of to_yaml().
88
+ #
89
+ # indent_space_count - The number of spaces to indent each line in the output (default: 0).
90
+ #
91
+ # Returns a String representation of the Kit in YAML format.
92
+ def to_yaml(indent_space_count = 0)
93
+ yaml = ""
94
+ longest_label_mapping_length =
95
+ @label_mappings.keys.inject(0) do |max_length, name|
96
+ (name.to_s.length > max_length) ? name.to_s.length : max_length
97
+ end
98
+
99
+ if @label_mappings.length > 0
100
+ yaml += " " * indent_space_count + "Kit:\n"
101
+ ljust_amount = longest_label_mapping_length + 1 # The +1 is for the trailing ":"
102
+ @label_mappings.sort.each do |label, path|
103
+ yaml += " " * indent_space_count + " - #{(label + ":").ljust(ljust_amount)} #{path}\n"
104
+ end
105
+ end
106
+
107
+ yaml
108
+ end
109
+
110
+ attr_reader :base_path, :label_mappings, :bits_per_sample, :num_channels
111
+
112
+ private
113
+
114
+ def load_sounds(base_path, kit_items)
115
+ # Set label mappings
116
+ kit_items.each do |label, sound_file_names|
117
+ if sound_file_names.class == Array
118
+ raise StandardError, "Composite sounds aren't allowed (yet...)"
119
+ end
120
+
121
+ unless label == sound_file_names
122
+ @label_mappings[label] = sound_file_names
123
+ end
124
+ end
125
+
126
+ kit_items = make_file_names_absolute(kit_items)
127
+ sound_buffers = load_raw_sounds(kit_items)
128
+
129
+ canonical_format = WaveFile::Format.new(@num_channels, @bits_per_sample, 44100)
130
+
131
+ # Convert each sound to a common format
132
+ sound_buffers.each {|file_name, buffer| sound_buffers[file_name] = buffer.convert(canonical_format) }
133
+
134
+ # If necessary, mix component sounds into a composite
135
+ kit_items.each do |label, sound_file_names|
136
+ @sound_bank[label] = mixdown(sound_file_names, sound_buffers)
137
+ end
138
+ end
139
+
140
+ # Converts relative paths into absolute paths. Note that this will also handle
141
+ # expanding ~ on platforms that support that.
142
+ def make_file_names_absolute(kit_items)
143
+ kit_items.each do |label, sound_file_names|
144
+ unless sound_file_names.class == Array
145
+ sound_file_names = [sound_file_names]
146
+ end
147
+
148
+ sound_file_names.map! {|sound_file_name| File.expand_path(sound_file_name, base_path) }
149
+ kit_items[label] = sound_file_names
150
+ end
151
+
152
+ kit_items
153
+ end
154
+
155
+ # Load all sound files, bailing if any are invalid
156
+ def load_raw_sounds(kit_items)
157
+ raw_sounds = {}
158
+ kit_items.values.flatten.each do |sound_file_name|
159
+ begin
160
+ info = WaveFile::Reader.info(sound_file_name)
161
+ WaveFile::Reader.new(sound_file_name).each_buffer(info.sample_frame_count) do |buffer|
162
+ raw_sounds[sound_file_name] = buffer
163
+ @num_channels = [@num_channels, buffer.channels].max
164
+ end
165
+ rescue Errno::ENOENT
166
+ raise SoundFileNotFoundError, "Sound file #{sound_file_name} not found."
167
+ rescue StandardError
168
+ raise InvalidSoundFormatError, "Sound file #{sound_file_name} is either not a sound file, " +
169
+ "or is in an unsupported format. BEATS can handle 8, 16, 24, or 32-bit PCM *.wav files."
170
+ end
171
+ end
172
+
173
+ raw_sounds
174
+ end
175
+
176
+ def mixdown(sound_file_names, raw_sounds)
177
+ sample_arrays = []
178
+ sound_file_names.each do |sound_file_name|
179
+ sample_arrays << raw_sounds[sound_file_name].samples
180
+ end
181
+
182
+ composited_sample_data = AudioUtils.composite(sample_arrays, @num_channels)
183
+
184
+ AudioUtils.scale(composited_sample_data, @num_channels, sound_file_names.length)
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,88 @@
1
+ module Beats
2
+ # Domain object which models one or more Tracks playing a part of the song at the same time.
3
+ # For example, a bass drum, snare drum, and hi-hat track playing the song's chorus.
4
+ #
5
+ # This object is like sheet music; the AudioEngine is responsible creating actual
6
+ # audio data for a Pattern (with the help of a Kit).
7
+ class Pattern
8
+ FLOW_TRACK_NAME = "flow"
9
+
10
+ def initialize(name)
11
+ @name = name
12
+ @tracks = {}
13
+ end
14
+
15
+ # Adds a new track to the pattern.
16
+ def track(name, rhythm)
17
+ track_key = unique_track_name(name)
18
+ new_track = Track.new(name, rhythm)
19
+ @tracks[track_key] = new_track
20
+
21
+ # If the new track is longer than any of the previously added tracks,
22
+ # pad the other tracks with trailing . to make them all the same length.
23
+ # Necessary to prevent incorrect overflow calculations for tracks.
24
+ longest_track_length = step_count()
25
+ @tracks.values.each do |track|
26
+ if track.rhythm.length < longest_track_length
27
+ track.rhythm += "." * (longest_track_length - track.rhythm.length)
28
+ end
29
+ end
30
+
31
+ new_track
32
+ end
33
+
34
+ def step_count
35
+ @tracks.values.collect {|track| track.rhythm.length }.max || 0
36
+ end
37
+
38
+ # Returns whether or not this pattern has the same number of tracks as other_pattern, and that
39
+ # each of the tracks has the same name and rhythm. Ordering of tracks does not matter; will
40
+ # return true if the two patterns have the same tracks but in a different ordering.
41
+ def same_tracks_as?(other_pattern)
42
+ @tracks.keys.each do |track_name|
43
+ other_pattern_track = other_pattern.tracks[track_name]
44
+ if other_pattern_track.nil? || @tracks[track_name].rhythm != other_pattern_track.rhythm
45
+ return false
46
+ end
47
+ end
48
+
49
+ @tracks.length == other_pattern.tracks.length
50
+ end
51
+
52
+ # Returns a YAML representation of the Pattern. Produces nicer looking output than the default
53
+ # version of to_yaml().
54
+ def to_yaml
55
+ longest_track_name_length =
56
+ @tracks.keys.inject(0) do |max_length, name|
57
+ (name.to_s.length > max_length) ? name.to_s.length : max_length
58
+ end
59
+ ljust_amount = longest_track_name_length + 7
60
+
61
+ yaml = "#{@name.to_s.capitalize}:\n"
62
+ @tracks.keys.sort.each do |track_name|
63
+ yaml += " - #{track_name}:".ljust(ljust_amount)
64
+ yaml += "#{@tracks[track_name].rhythm}\n"
65
+ end
66
+
67
+ yaml
68
+ end
69
+
70
+ attr_accessor :tracks, :name
71
+
72
+ private
73
+
74
+ # Returns a unique track name that is not already in use by a track in
75
+ # this pattern. Used to help support having multiple tracks with the same
76
+ # sample in a track.
77
+ def unique_track_name(name)
78
+ i = 2
79
+ name_key = name
80
+ while @tracks.has_key? name_key
81
+ name_key = "#{name}#{i.to_s}"
82
+ i += 1
83
+ end
84
+
85
+ name_key
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,162 @@
1
+ module Beats
2
+ class InvalidTempoError < RuntimeError; end
3
+
4
+
5
+ # Domain object which models the 'sheet music' for a full song. Models the Patterns
6
+ # that should be played, in which order (i.e. the flow), and at which tempo.
7
+ #
8
+ # This is the top-level model object that is used by the AudioEngine to produce
9
+ # actual audio data. A Song tells the AudioEngine what sounds to trigger and when.
10
+ # A Kit provides the sample data for each of these sounds. With a Song and a Kit
11
+ # the AudioEngine can produce the audio data that is saved to disk.
12
+ class Song
13
+ DEFAULT_TEMPO = 120
14
+
15
+ def initialize
16
+ self.tempo = DEFAULT_TEMPO
17
+ @patterns = {}
18
+ @flow = []
19
+ end
20
+
21
+ # Adds a new pattern to the song, with the specified name.
22
+ def pattern(name)
23
+ @patterns[name] = Pattern.new(name)
24
+ @patterns[name]
25
+ end
26
+
27
+
28
+ # The number of tracks that the pattern with the greatest number of tracks has.
29
+ # TODO: Is it a problem that an optimized song can have a different total_tracks() value than
30
+ # the original? Or is that actually a good thing?
31
+ # TODO: Investigate replacing this with a method max_sounds_playing_at_once() or something
32
+ # like that. Would look each pattern along with it's incoming overflow.
33
+ def total_tracks
34
+ @patterns.keys.collect {|pattern_name| @patterns[pattern_name].tracks.length }.max || 0
35
+ end
36
+
37
+ # The unique track names used in each of the song's patterns. Sorted in alphabetical order.
38
+ # For example calling this method for this song:
39
+ #
40
+ # Verse:
41
+ # - bass: X...
42
+ # - snare: ..X.
43
+ #
44
+ # Chorus:
45
+ # - bass: X.X.
46
+ # - snare: X.X.
47
+ # - hihat: XXXX
48
+ #
49
+ # Will return: ["bass", "hihat", "snare"]
50
+ def track_names
51
+ @patterns.values.inject([]) {|track_names, pattern| track_names | pattern.tracks.keys }.sort
52
+ end
53
+
54
+ def tempo=(new_tempo)
55
+ unless new_tempo.class == Fixnum && new_tempo > 0
56
+ raise InvalidTempoError, "Invalid tempo: '#{new_tempo}'. Tempo must be a number greater than 0."
57
+ end
58
+
59
+ @tempo = new_tempo
60
+ end
61
+
62
+ # Returns a new Song that is identical but with no patterns or flow.
63
+ def copy_ignoring_patterns_and_flow
64
+ copy = Song.new()
65
+ copy.tempo = @tempo
66
+
67
+ copy
68
+ end
69
+
70
+ # Splits a Song object into multiple Song objects, where each new
71
+ # Song only has 1 track. For example, if a Song has 5 tracks, this will return
72
+ # a hash of 5 songs, each with one of the original Song's tracks.
73
+ def split
74
+ split_songs = {}
75
+ track_names = track_names()
76
+
77
+ track_names.each do |track_name|
78
+ new_song = copy_ignoring_patterns_and_flow()
79
+
80
+ @patterns.each do |name, original_pattern|
81
+ new_pattern = new_song.pattern(name)
82
+
83
+ if original_pattern.tracks.has_key?(track_name)
84
+ original_track = original_pattern.tracks[track_name]
85
+ new_pattern.track(original_track.name, original_track.rhythm)
86
+ else
87
+ new_pattern.track(track_name, "." * original_pattern.step_count)
88
+ end
89
+ end
90
+
91
+ new_song.flow = @flow
92
+
93
+ split_songs[track_name] = new_song
94
+ end
95
+
96
+ split_songs
97
+ end
98
+
99
+ # Removes any patterns that aren't referenced in the flow.
100
+ def remove_unused_patterns
101
+ # Using reject() here because for some reason select() returns an Array not a Hash.
102
+ @patterns.reject! {|k, pattern| !@flow.member?(pattern.name) }
103
+ end
104
+
105
+ # Serializes the current Song to a YAML string. This string can then be used to construct a new Song
106
+ # using the SongParser class. This lets you save a Song to disk, to be re-loaded later. Produces nicer
107
+ # looking output than the default version of to_yaml().
108
+ def to_yaml(kit)
109
+ # This implementation intentionally builds up a YAML string manually instead of using YAML::dump().
110
+ # Ruby 1.8 makes it difficult to ensure a consistent ordering of hash keys, which makes the output ugly
111
+ # and also hard to test.
112
+
113
+ yaml_output = "Song:\n"
114
+ yaml_output += " Tempo: #{@tempo}\n"
115
+ yaml_output += flow_to_yaml()
116
+ yaml_output += kit.to_yaml(2)
117
+ yaml_output += patterns_to_yaml()
118
+
119
+ yaml_output
120
+ end
121
+
122
+ attr_reader :patterns, :tempo
123
+ attr_accessor :flow
124
+
125
+ private
126
+
127
+ def longest_length_in_array(arr)
128
+ arr.inject(0) {|max_length, name| [name.to_s.length, max_length].max }
129
+ end
130
+
131
+ def flow_to_yaml
132
+ yaml_output = " Flow:\n"
133
+ ljust_amount = longest_length_in_array(@flow) + 1 # The +1 is for the trailing ":"
134
+ previous = nil
135
+ count = 0
136
+ @flow.each do |pattern_name|
137
+ if pattern_name == previous || previous.nil?
138
+ count += 1
139
+ else
140
+ yaml_output += " - #{(previous.to_s.capitalize + ':').ljust(ljust_amount)} x#{count}\n"
141
+ count = 1
142
+ end
143
+ previous = pattern_name
144
+ end
145
+ yaml_output += " - #{(previous.to_s.capitalize + ':').ljust(ljust_amount)} x#{count}\n"
146
+
147
+ yaml_output
148
+ end
149
+
150
+ def patterns_to_yaml
151
+ yaml_output = ""
152
+
153
+ # Sort to ensure a consistent order, to make testing easier
154
+ pattern_names = @patterns.keys.map {|key| key.to_s} # Ruby 1.8 can't sort symbols...
155
+ pattern_names.sort.each do |pattern_name|
156
+ yaml_output += "\n" + @patterns[pattern_name.to_sym].to_yaml()
157
+ end
158
+
159
+ yaml_output
160
+ end
161
+ end
162
+ end