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.
@@ -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