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,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