beats 1.2.4 → 1.2.5

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