class Beats::AudioEngine
This class actually generates the output audio data that is saved to disk.
To produce audio data, it needs two things: a Song
and a Kit
. The Song
tells it which sounds to trigger and when, while the Kit
provides the sample data for each of these sounds.
Example usage, assuming song and kit are already defined:
engine = AudioEngine.new(song, kit) engine.write_to_file("my_song.wav")
Constants
- SAMPLE_RATE
Attributes
Public Class Methods
# File lib/beats/audio_engine.rb, line 16 def initialize(song, kit) @song = song @kit = kit @step_sample_length = AudioUtils.step_sample_length(SAMPLE_RATE, @song.tempo) @composited_pattern_cache = {} end
Public Instance Methods
# File lib/beats/audio_engine.rb, line 24 def write_to_file(output_file_name) packed_pattern_cache = {} num_tracks_in_song = @song.total_tracks # Open output wave file and prepare it for writing sample data. format = WaveFile::Format.new(@kit.num_channels, "pcm_#{@kit.bits_per_sample}".to_sym, SAMPLE_RATE) writer = WaveFile::CachingWriter.new(output_file_name, format) # Generate each pattern's sample data, or pull it from cache, and append it to the wave file. incoming_overflow = {} @song.flow.each do |pattern_name| key = [pattern_name, incoming_overflow.hash] unless packed_pattern_cache.member?(key) sample_data = generate_pattern_sample_data(@song.patterns[pattern_name], incoming_overflow) packed_pattern_cache[key] = { primary: WaveFile::Buffer.new(sample_data[:primary], format), overflow: WaveFile::Buffer.new(sample_data[:overflow], format) } end writer.write(packed_pattern_cache[key][:primary]) incoming_overflow = packed_pattern_cache[key][:overflow].samples end # Write any remaining overflow from the final pattern final_overflow_composite = AudioUtils.composite(incoming_overflow.values, format.channels) final_overflow_composite = AudioUtils.scale(final_overflow_composite, format.channels, num_tracks_in_song) writer.write(WaveFile::Buffer.new(final_overflow_composite, format)) writer.close writer.total_duration end
Private Instance Methods
# File lib/beats/audio_engine.rb, line 111 def composite_pattern_tracks(pattern) overflow_sample_data = {} raw_track_sample_arrays = [] pattern.tracks.each do |track_name, track| temp = generate_track_sample_data(track, @kit.get_sample_data(track.name)) raw_track_sample_arrays << temp[:primary] overflow_sample_data[track_name] = temp[:overflow] end primary_sample_data = AudioUtils.composite(raw_track_sample_arrays, @kit.num_channels) return primary_sample_data, overflow_sample_data end
Composites the sample data for each of the pattern's tracks, and returns the overflow sample data from tracks whose last sound trigger extends past the end of the pattern. This overflow can be used by the next pattern to avoid sounds cutting off when the pattern changes.
# File lib/beats/audio_engine.rb, line 91 def generate_pattern_sample_data(pattern, incoming_overflow) # Unless cached, composite each track's sample data. if @composited_pattern_cache[pattern].nil? primary_sample_data, overflow_sample_data = composite_pattern_tracks(pattern) @composited_pattern_cache[pattern] = {primary: primary_sample_data.dup, overflow: overflow_sample_data.dup} else primary_sample_data = @composited_pattern_cache[pattern][:primary].dup overflow_sample_data = @composited_pattern_cache[pattern][:overflow].dup end # Composite overflow from the previous pattern onto this pattern, to prevent sounds from cutting off. primary_sample_data, overflow_sample_data = handle_incoming_overflow(pattern, incoming_overflow, primary_sample_data, overflow_sample_data) primary_sample_data = AudioUtils.scale(primary_sample_data, @kit.num_channels, @song.total_tracks) {primary: primary_sample_data, overflow: overflow_sample_data} end
Generates the sample data for a single track, using the specified sound's sample data.
# File lib/beats/audio_engine.rb, line 62 def generate_track_sample_data(track, sound) trigger_step_lengths = track.trigger_step_lengths if trigger_step_lengths == [0] return {primary: [], overflow: []} # Is this really what should happen? Why throw away overflow? end fill_value = (@kit.num_channels == 1) ? 0 : Array.new(@kit.num_channels, 0) primary_sample_data = [].fill(fill_value, 0, AudioUtils.step_start_sample(track.step_count, @step_sample_length)) step_index = trigger_step_lengths[0] trigger_sample_length = 0 trigger_step_lengths[1...(trigger_step_lengths.length)].each do |trigger_step_length| start_sample = AudioUtils.step_start_sample(step_index, @step_sample_length) end_sample = [(start_sample + sound.length), primary_sample_data.length].min trigger_sample_length = end_sample - start_sample primary_sample_data[start_sample...end_sample] = sound[0...trigger_sample_length] step_index += trigger_step_length end overflow_sample_data = (sound == [] || trigger_step_lengths.length == 1) ? [] : sound[trigger_sample_length...(sound.length)] {primary: primary_sample_data, overflow: overflow_sample_data} end
Applies sound overflow (i.e. long sounds such as cymbal crash which extend past the last step) from the previous pattern in the flow to the current pattern. This prevents sounds from being cut off when the pattern changes.
It would probably be shorter and conceptually simpler to deal with incoming overflow in generate_track_sample_data
() instead of this method. (In fact, this method would go away). However, doing it this way allows for caching composited pattern sample data, and applying incoming overflow to the composite. This allows each pattern to only be composited once, regardless of the incoming overflow that each performance of it receives. If incoming overflow was handled at the Track
level we couldn't do that.
# File lib/beats/audio_engine.rb, line 135 def handle_incoming_overflow(pattern, incoming_overflow, primary_sample_data, overflow_sample_data) pattern_track_names = pattern.tracks.keys sample_arrays = [primary_sample_data] incoming_overflow.each do |incoming_track_name, incoming_sample_data| end_sample = incoming_sample_data.length if pattern_track_names.member?(incoming_track_name) track = pattern.tracks[incoming_track_name] if track.trigger_step_lengths.length > 1 intro_length = (pattern.tracks[incoming_track_name].trigger_step_lengths[0] * step_sample_length).floor end_sample = [end_sample, intro_length].min end end if end_sample > primary_sample_data.length end_sample = primary_sample_data.length overflow_sample_data[incoming_track_name] = incoming_sample_data[(primary_sample_data.length)...(incoming_sample_data.length)] end sample_arrays << incoming_sample_data[0...end_sample] end return AudioUtils.composite(sample_arrays, @kit.num_channels), overflow_sample_data end