beats 1.0.0 → 1.1.0
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.
- data/README.markdown +14 -9
- data/bin/beats +4 -8
- data/lib/kit.rb +27 -13
- data/lib/pattern.rb +15 -4
- data/lib/song.rb +10 -110
- data/lib/songparser.rb +169 -0
- data/test/includes.rb +1 -0
- data/test/kit_test.rb +22 -17
- data/test/pattern_test.rb +5 -5
- data/test/song_test.rb +13 -99
- data/test/songparser_test.rb +207 -0
- metadata +5 -2
    
        data/README.markdown
    CHANGED
    
    | @@ -10,18 +10,23 @@ BEATS is a drum machine written in pure Ruby. Feed it a song notated in YAML, an | |
| 10 10 | 
             
                    - Chorus:  x4
         | 
| 11 11 | 
             
                    - Verse:   x2
         | 
| 12 12 | 
             
                    - Chorus:  x4
         | 
| 13 | 
            +
                  Kit:
         | 
| 14 | 
            +
                    - bass:       sounds/bass.wav
         | 
| 15 | 
            +
            		- snare:      sounds/snare.wav
         | 
| 16 | 
            +
            		- hh_closed:  sounds/hh_closed.wav
         | 
| 17 | 
            +
            		- agogo:      sounds/agogo_high.wav
         | 
| 13 18 |  | 
| 14 19 | 
             
                Verse:
         | 
| 15 | 
            -
                  - bass | 
| 16 | 
            -
                  - snare | 
| 17 | 
            -
                  - hh_closed | 
| 18 | 
            -
                  -  | 
| 20 | 
            +
                  - bass:             X...X...X...X...
         | 
| 21 | 
            +
                  - snare:            ..............X.
         | 
| 22 | 
            +
                  - hh_closed:        X.XXX.XXX.X.X.X.
         | 
| 23 | 
            +
                  - agogo:            ..............XX
         | 
| 19 24 |  | 
| 20 25 | 
             
                Chorus:
         | 
| 21 | 
            -
                  - bass | 
| 22 | 
            -
                  - snare | 
| 23 | 
            -
                  - hh_closed | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            +
                  - bass:             X...X...X...X...
         | 
| 27 | 
            +
                  - snare:            ....X.......X...
         | 
| 28 | 
            +
                  - hh_closed:        X.XXX.XXX.XX..X.
         | 
| 29 | 
            +
            	  - sounds/tom4.wav:  ...........X....
         | 
| 30 | 
            +
            	  - sounds/tom2.wav:  ..............X.
         | 
| 26 31 |  | 
| 27 32 | 
             
            For installation and usage instructions, visit the BEATS website at [http://beatsdrummachine.com](http://beatsdrummachine.com).
         | 
    
        data/bin/beats
    CHANGED
    
    | @@ -1,6 +1,7 @@ | |
| 1 1 | 
             
            #!/usr/bin/env ruby
         | 
| 2 2 |  | 
| 3 3 | 
             
            require File.dirname(__FILE__) + "/../lib/song"
         | 
| 4 | 
            +
            require File.dirname(__FILE__) + "/../lib/songparser"
         | 
| 4 5 | 
             
            require File.dirname(__FILE__) + "/../lib/kit"
         | 
| 5 6 | 
             
            require File.dirname(__FILE__) + "/../lib/pattern"
         | 
| 6 7 | 
             
            require File.dirname(__FILE__) + "/../lib/track"
         | 
| @@ -9,7 +10,7 @@ require "optparse" | |
| 9 10 | 
             
            require "yaml"
         | 
| 10 11 | 
             
            require "wavefile"
         | 
| 11 12 |  | 
| 12 | 
            -
            BEATS_VERSION = "1. | 
| 13 | 
            +
            BEATS_VERSION = "1.1.0"
         | 
| 13 14 | 
             
            SAMPLE_RATE = 44100
         | 
| 14 15 |  | 
| 15 16 | 
             
            def parse_options
         | 
| @@ -63,7 +64,8 @@ end | |
| 63 64 |  | 
| 64 65 | 
             
            begin
         | 
| 65 66 | 
             
              parse_start_time = Time.now
         | 
| 66 | 
            -
               | 
| 67 | 
            +
              song_parser = SongParser.new()
         | 
| 68 | 
            +
              song_from_file = song_parser.parse(File.dirname(input_file), YAML.load_file(input_file))
         | 
| 67 69 |  | 
| 68 70 | 
             
              generate_samples_start = Time.now
         | 
| 69 71 | 
             
              sample_data = song_from_file.sample_data(options[:pattern], options[:split])
         | 
| @@ -90,12 +92,6 @@ rescue Errno::ENOENT => detail | |
| 90 92 | 
             
              puts ""
         | 
| 91 93 | 
             
              puts "Song file '#{input_file}' not found."
         | 
| 92 94 | 
             
              puts ""
         | 
| 93 | 
            -
            rescue ArgumentError => detail
         | 
| 94 | 
            -
              puts ""
         | 
| 95 | 
            -
              puts "Song file '#{input_file}' has an error:"
         | 
| 96 | 
            -
              puts "  Syntax error in YAML file:"
         | 
| 97 | 
            -
              puts "  #{detail}"
         | 
| 98 | 
            -
              puts ""
         | 
| 99 95 | 
             
            rescue SongParseError => detail
         | 
| 100 96 | 
             
              puts ""
         | 
| 101 97 | 
             
              puts "Song file '#{input_file}' has an error:"
         | 
    
        data/lib/kit.rb
    CHANGED
    
    | @@ -1,5 +1,10 @@ | |
| 1 | 
            +
            class SoundNotFoundError < RuntimeError; end
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            class Kit
         | 
| 2 | 
            -
               | 
| 4 | 
            +
              PATH_SEPARATOR = File.const_get("SEPARATOR")
         | 
| 5 | 
            +
              
         | 
| 6 | 
            +
              def initialize(base_path)
         | 
| 7 | 
            +
                @base_path = base_path
         | 
| 3 8 | 
             
                @sounds = {}
         | 
| 4 9 | 
             
                @num_channels = 0
         | 
| 5 10 | 
             
                @bits_per_sample = 0
         | 
| @@ -7,28 +12,37 @@ class Kit | |
| 7 12 |  | 
| 8 13 | 
             
              def add(name, path)
         | 
| 9 14 | 
             
                if(!@sounds.has_key? name)
         | 
| 10 | 
            -
                   | 
| 11 | 
            -
             | 
| 15 | 
            +
                  if(!path.start_with?(PATH_SEPARATOR))
         | 
| 16 | 
            +
                    path = @base_path + PATH_SEPARATOR + path
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                  
         | 
| 19 | 
            +
                  begin
         | 
| 20 | 
            +
                    wavefile = WaveFile.open(path)
         | 
| 21 | 
            +
                  rescue
         | 
| 22 | 
            +
                    raise SoundNotFoundError, "Sound file #{name} not found."
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                  
         | 
| 25 | 
            +
                  @sounds[name] = wavefile
         | 
| 12 26 |  | 
| 13 | 
            -
                  if  | 
| 14 | 
            -
                    @num_channels =  | 
| 27 | 
            +
                  if wavefile.num_channels > @num_channels
         | 
| 28 | 
            +
                    @num_channels = wavefile.num_channels
         | 
| 15 29 | 
             
                  end
         | 
| 16 | 
            -
                  if  | 
| 17 | 
            -
                    @bits_per_sample =  | 
| 30 | 
            +
                  if wavefile.bits_per_sample > @bits_per_sample
         | 
| 31 | 
            +
                    @bits_per_sample = wavefile.bits_per_sample
         | 
| 18 32 | 
             
                  end
         | 
| 19 33 | 
             
                end
         | 
| 20 34 | 
             
              end
         | 
| 21 35 |  | 
| 22 36 | 
             
              def get_sample_data(name)
         | 
| 23 | 
            -
                 | 
| 37 | 
            +
                wavefile = @sounds[name]
         | 
| 24 38 |  | 
| 25 | 
            -
                if  | 
| 39 | 
            +
                if wavefile == nil
         | 
| 26 40 | 
             
                  raise StandardError, "Kit doesn't contain sound '#{name}'."
         | 
| 27 41 | 
             
                else
         | 
| 28 | 
            -
                   | 
| 29 | 
            -
                   | 
| 42 | 
            +
                  wavefile.num_channels = @num_channels
         | 
| 43 | 
            +
                  wavefile.bits_per_sample = @bits_per_sample
         | 
| 30 44 |  | 
| 31 | 
            -
                  return  | 
| 45 | 
            +
                  return wavefile.sample_data
         | 
| 32 46 | 
             
                end
         | 
| 33 47 | 
             
              end
         | 
| 34 48 |  | 
| @@ -36,5 +50,5 @@ class Kit | |
| 36 50 | 
             
                return @sounds.length
         | 
| 37 51 | 
             
              end
         | 
| 38 52 |  | 
| 39 | 
            -
              attr_reader :bits_per_sample, :num_channels
         | 
| 53 | 
            +
              attr_reader :base_path, :bits_per_sample, :num_channels
         | 
| 40 54 | 
             
            end
         | 
    
        data/lib/pattern.rb
    CHANGED
    
    | @@ -1,6 +1,7 @@ | |
| 1 1 | 
             
            class Pattern
         | 
| 2 2 | 
             
              def initialize(name)
         | 
| 3 3 | 
             
                @name = name
         | 
| 4 | 
            +
                @cache = {}
         | 
| 4 5 | 
             
                @tracks = {}
         | 
| 5 6 | 
             
              end
         | 
| 6 7 |  | 
| @@ -47,6 +48,12 @@ class Pattern | |
| 47 48 | 
             
            private
         | 
| 48 49 |  | 
| 49 50 | 
             
              def combined_sample_data(tick_sample_length, num_channels, num_tracks_in_song, incoming_overflow)
         | 
| 51 | 
            +
                # If we've already encountered this pattern with the same incoming overflow before,
         | 
| 52 | 
            +
                # return the pre-mixed down version from the cache.
         | 
| 53 | 
            +
                if(@cache.member?(incoming_overflow))
         | 
| 54 | 
            +
                  return @cache[incoming_overflow]
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
                
         | 
| 50 57 | 
             
                fill_value = (num_channels == 1) ? 0 : [].fill(0, 0, num_channels)
         | 
| 51 58 | 
             
                track_names = @tracks.keys
         | 
| 52 59 | 
             
                primary_sample_data = []
         | 
| @@ -64,8 +71,8 @@ private | |
| 64 71 | 
             
                      (0...track_samples.length).each {|i| primary_sample_data[i] += track_samples[i] }
         | 
| 65 72 | 
             
                    else
         | 
| 66 73 | 
             
                      (0...track_samples.length).each {|i|
         | 
| 67 | 
            -
                        primary_sample_data[i] | 
| 68 | 
            -
             | 
| 74 | 
            +
                        primary_sample_data[i][0] += track_samples[i][0]
         | 
| 75 | 
            +
                        primary_sample_data[i][1] += track_samples[i][1]
         | 
| 69 76 | 
             
                      }
         | 
| 70 77 | 
             
                    end
         | 
| 71 78 |  | 
| @@ -86,14 +93,18 @@ private | |
| 86 93 | 
             
                  end
         | 
| 87 94 | 
             
                }
         | 
| 88 95 |  | 
| 89 | 
            -
                # Mix down the tracks into one
         | 
| 96 | 
            +
                # Mix down the pattern's tracks into one single track
         | 
| 90 97 | 
             
                if(num_channels == 1)
         | 
| 91 98 | 
             
                  primary_sample_data = primary_sample_data.map {|sample| (sample / num_tracks_in_song).round }
         | 
| 92 99 | 
             
                else
         | 
| 93 100 | 
             
                  primary_sample_data = primary_sample_data.map {|sample| [(sample[0] / num_tracks_in_song).round, (sample[1] / num_tracks_in_song).round] }
         | 
| 94 101 | 
             
                end
         | 
| 95 102 |  | 
| 96 | 
            -
                 | 
| 103 | 
            +
                # Add the result to the cache so we don't have to go through all of this the next time...
         | 
| 104 | 
            +
                mixdown_sample_data = {:primary => primary_sample_data, :overflow => overflow_sample_data}
         | 
| 105 | 
            +
                @cache[incoming_overflow] = mixdown_sample_data
         | 
| 106 | 
            +
                
         | 
| 107 | 
            +
                return mixdown_sample_data
         | 
| 97 108 | 
             
              end
         | 
| 98 109 |  | 
| 99 110 | 
             
              def split_sample_data(tick_sample_length, num_channels, incoming_overflow)
         | 
    
        data/lib/song.rb
    CHANGED
    
    | @@ -1,20 +1,16 @@ | |
| 1 | 
            -
            class  | 
| 1 | 
            +
            class InvalidTempoError < RuntimeError; end
         | 
| 2 2 |  | 
| 3 3 | 
             
            class Song
         | 
| 4 4 | 
             
              SAMPLE_RATE = 44100
         | 
| 5 5 | 
             
              SECONDS_PER_MINUTE = 60.0
         | 
| 6 | 
            -
               | 
| 6 | 
            +
              SAMPLES_PER_MINUTE = SAMPLE_RATE * SECONDS_PER_MINUTE
         | 
| 7 | 
            +
              DEFAULT_TEMPO = 120
         | 
| 7 8 |  | 
| 8 | 
            -
              def initialize( | 
| 9 | 
            -
                self.tempo =  | 
| 10 | 
            -
                @ | 
| 11 | 
            -
                @kit = Kit.new()
         | 
| 9 | 
            +
              def initialize(base_path)
         | 
| 10 | 
            +
                self.tempo = DEFAULT_TEMPO
         | 
| 11 | 
            +
                @kit = Kit.new(base_path)
         | 
| 12 12 | 
             
                @patterns = {}
         | 
| 13 13 | 
             
                @structure = []
         | 
| 14 | 
            -
             | 
| 15 | 
            -
                if(definition != nil)
         | 
| 16 | 
            -
                  parse(definition)
         | 
| 17 | 
            -
                end
         | 
| 18 14 | 
             
              end
         | 
| 19 15 |  | 
| 20 16 | 
             
              def pattern(name)
         | 
| @@ -86,15 +82,15 @@ class Song | |
| 86 82 |  | 
| 87 83 | 
             
              def tempo=(new_tempo)
         | 
| 88 84 | 
             
                if(new_tempo.class != Fixnum || new_tempo <= 0)
         | 
| 89 | 
            -
                  raise  | 
| 85 | 
            +
                  raise InvalidTempoError, "Invalid tempo: '#{new_tempo}'. Tempo must be a number greater than 0."
         | 
| 90 86 | 
             
                end
         | 
| 91 87 |  | 
| 92 88 | 
             
                @tempo = new_tempo
         | 
| 93 | 
            -
                @tick_sample_length =  | 
| 89 | 
            +
                @tick_sample_length = SAMPLES_PER_MINUTE / new_tempo / 4.0
         | 
| 94 90 | 
             
              end
         | 
| 95 91 |  | 
| 96 | 
            -
              attr_reader : | 
| 97 | 
            -
              attr_accessor :structure
         | 
| 92 | 
            +
              attr_reader :tick_sample_length, :patterns
         | 
| 93 | 
            +
              attr_accessor :structure, :kit
         | 
| 98 94 |  | 
| 99 95 | 
             
            private
         | 
| 100 96 |  | 
| @@ -117,102 +113,6 @@ private | |
| 117 113 |  | 
| 118 114 | 
             
                return merged_sample_data
         | 
| 119 115 | 
             
              end
         | 
| 120 | 
            -
             | 
| 121 | 
            -
              # Converts all hash keys to be lowercase
         | 
| 122 | 
            -
              def downcase_hash_keys(hash)
         | 
| 123 | 
            -
                return hash.inject({}) {|new_hash, pair|
         | 
| 124 | 
            -
                    new_hash[pair.first.downcase] = pair.last
         | 
| 125 | 
            -
                    new_hash
         | 
| 126 | 
            -
                }
         | 
| 127 | 
            -
              end
         | 
| 128 | 
            -
             | 
| 129 | 
            -
              def parse(definition)
         | 
| 130 | 
            -
                if(definition.class == String)
         | 
| 131 | 
            -
                  song_definition = YAML.load(definition)
         | 
| 132 | 
            -
                elsif(definition.class == Hash)
         | 
| 133 | 
            -
                  song_definition = definition
         | 
| 134 | 
            -
                else
         | 
| 135 | 
            -
                  raise StandardError, "Invalid song input"
         | 
| 136 | 
            -
                end
         | 
| 137 | 
            -
             | 
| 138 | 
            -
                @kit = build_kit(song_definition)
         | 
| 139 | 
            -
             | 
| 140 | 
            -
                song_definition = downcase_hash_keys(song_definition)
         | 
| 141 | 
            -
                
         | 
| 142 | 
            -
                # Process each pattern
         | 
| 143 | 
            -
                song_definition.keys.each{|key|
         | 
| 144 | 
            -
                  if(key != "song")
         | 
| 145 | 
            -
                    new_pattern = self.pattern key.to_sym
         | 
| 146 | 
            -
             | 
| 147 | 
            -
                    track_list = song_definition[key]
         | 
| 148 | 
            -
                    track_list.each{|track_definition|
         | 
| 149 | 
            -
                      track_name = track_definition.keys.first
         | 
| 150 | 
            -
                      new_pattern.track track_name, @kit.get_sample_data(track_name), track_definition[track_name]
         | 
| 151 | 
            -
                    }
         | 
| 152 | 
            -
                  end
         | 
| 153 | 
            -
                }
         | 
| 154 | 
            -
                
         | 
| 155 | 
            -
                # Process song header
         | 
| 156 | 
            -
                parse_song_header(downcase_hash_keys(song_definition["song"]))
         | 
| 157 | 
            -
              end
         | 
| 158 | 
            -
              
         | 
| 159 | 
            -
              def parse_song_header(header_data)
         | 
| 160 | 
            -
                self.tempo = header_data["tempo"]
         | 
| 161 | 
            -
             | 
| 162 | 
            -
                pattern_list = header_data["structure"]
         | 
| 163 | 
            -
                structure = []
         | 
| 164 | 
            -
                pattern_list.each{|pattern_item|
         | 
| 165 | 
            -
                  if(pattern_item.class == String)
         | 
| 166 | 
            -
                    pattern_item = {pattern_item => "x1"}
         | 
| 167 | 
            -
                  end
         | 
| 168 | 
            -
                  
         | 
| 169 | 
            -
                  pattern_name = pattern_item.keys.first
         | 
| 170 | 
            -
                  pattern_name_sym = pattern_name.downcase.to_sym
         | 
| 171 | 
            -
                  
         | 
| 172 | 
            -
                  if(!@patterns.has_key?(pattern_name_sym))
         | 
| 173 | 
            -
                    raise SongParseError, "Song structure includes non-existant pattern: #{pattern_name}."
         | 
| 174 | 
            -
                  end
         | 
| 175 | 
            -
                  
         | 
| 176 | 
            -
                  multiples_str = pattern_item[pattern_name]
         | 
| 177 | 
            -
                  multiples_str.slice!(0)
         | 
| 178 | 
            -
                  multiples = multiples_str.to_i
         | 
| 179 | 
            -
                  
         | 
| 180 | 
            -
                  if(multiples_str.match(/[^0-9]/) != nil)
         | 
| 181 | 
            -
                    raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Number of repeats should be a whole number."
         | 
| 182 | 
            -
                  elsif(multiples < 0)
         | 
| 183 | 
            -
                    raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Must be 0 or greater."
         | 
| 184 | 
            -
                  end
         | 
| 185 | 
            -
                  
         | 
| 186 | 
            -
                  multiples.times { structure << pattern_name_sym }
         | 
| 187 | 
            -
                }
         | 
| 188 | 
            -
             | 
| 189 | 
            -
                @structure = structure
         | 
| 190 | 
            -
              end
         | 
| 191 | 
            -
              
         | 
| 192 | 
            -
              def build_kit(song_definition)
         | 
| 193 | 
            -
                kit = Kit.new()
         | 
| 194 | 
            -
                
         | 
| 195 | 
            -
                song_definition.keys.each{|key|
         | 
| 196 | 
            -
                  if(key.downcase != "song")
         | 
| 197 | 
            -
                    track_list = song_definition[key]
         | 
| 198 | 
            -
                    track_list.each{|track_definition|
         | 
| 199 | 
            -
                      track_name = track_definition.keys.first
         | 
| 200 | 
            -
                      track_path = track_name
         | 
| 201 | 
            -
                      if(!track_path.start_with?(PATH_SEPARATOR))
         | 
| 202 | 
            -
                        track_path = @input_path + PATH_SEPARATOR + track_path
         | 
| 203 | 
            -
                      end
         | 
| 204 | 
            -
                      
         | 
| 205 | 
            -
                      if(!File.exists? track_path)
         | 
| 206 | 
            -
                        raise SongParseError, "File '#{track_name}' not found for pattern '#{key}'"
         | 
| 207 | 
            -
                      end
         | 
| 208 | 
            -
                      
         | 
| 209 | 
            -
                      kit.add(track_name, track_path)
         | 
| 210 | 
            -
                    }
         | 
| 211 | 
            -
                  end
         | 
| 212 | 
            -
                }
         | 
| 213 | 
            -
                
         | 
| 214 | 
            -
                return kit
         | 
| 215 | 
            -
              end
         | 
| 216 116 |  | 
| 217 117 | 
             
              def sample_data_split_all_patterns(fill_value, num_tracks_in_song)
         | 
| 218 118 | 
             
                output_data = {}
         | 
    
        data/lib/songparser.rb
    ADDED
    
    | @@ -0,0 +1,169 @@ | |
| 1 | 
            +
            class SongParseError < RuntimeError; end
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class SongParser
         | 
| 4 | 
            +
              NO_SONG_HEADER_ERROR_MSG =
         | 
| 5 | 
            +
            "Song must have a header. Here's an example:
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              Song:
         | 
| 8 | 
            +
                Tempo: 120
         | 
| 9 | 
            +
                Structure:
         | 
| 10 | 
            +
                  - Verse: x2
         | 
| 11 | 
            +
                  - Chorus: x2"
         | 
| 12 | 
            +
              
         | 
| 13 | 
            +
              def initialize()
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
              
         | 
| 16 | 
            +
              def parse(base_path, definition = nil)
         | 
| 17 | 
            +
                raw_song_definition = canonicalize_definition(definition)
         | 
| 18 | 
            +
                raw_song_components = split_raw_yaml_into_components(raw_song_definition)
         | 
| 19 | 
            +
                
         | 
| 20 | 
            +
                song = Song.new(base_path)
         | 
| 21 | 
            +
                
         | 
| 22 | 
            +
                # 1.) Set tempo
         | 
| 23 | 
            +
                begin
         | 
| 24 | 
            +
                  if raw_song_components[:tempo] != nil
         | 
| 25 | 
            +
                    song.tempo = raw_song_components[:tempo]
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                rescue InvalidTempoError => detail
         | 
| 28 | 
            +
                  raise SongParseError, "#{detail}"
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
                
         | 
| 31 | 
            +
                # 2.) Build the kit
         | 
| 32 | 
            +
                begin
         | 
| 33 | 
            +
                  kit = build_kit(base_path, raw_song_components[:kit], raw_song_components[:patterns])
         | 
| 34 | 
            +
                rescue SoundNotFoundError => detail
         | 
| 35 | 
            +
                  raise SongParseError, "#{detail}"
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
                song.kit = kit
         | 
| 38 | 
            +
                
         | 
| 39 | 
            +
                # 3.) Load patterns
         | 
| 40 | 
            +
                add_patterns_to_song(song, raw_song_components[:patterns])
         | 
| 41 | 
            +
                
         | 
| 42 | 
            +
                # 4.) Set structure
         | 
| 43 | 
            +
                if(raw_song_components[:structure] == nil)
         | 
| 44 | 
            +
                  raise SongParseError, "Song must have a Structure section in the header."
         | 
| 45 | 
            +
                else
         | 
| 46 | 
            +
                  set_song_structure(song, raw_song_components[:structure])
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
                
         | 
| 49 | 
            +
                return song
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
              
         | 
| 52 | 
            +
            private
         | 
| 53 | 
            +
             | 
| 54 | 
            +
              # This is basically a factory. Don't see a benefit to extracting to a full class.
         | 
| 55 | 
            +
              # Also, is "canonicalize" a word?
         | 
| 56 | 
            +
              def canonicalize_definition(definition)
         | 
| 57 | 
            +
                if(definition.class == String)
         | 
| 58 | 
            +
                  begin
         | 
| 59 | 
            +
                    raw_song_definition = YAML.load(definition)
         | 
| 60 | 
            +
                  rescue ArgumentError => detail
         | 
| 61 | 
            +
                    raise SongParseError, "Syntax error in YAML file"
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
                elsif(definition.class == Hash)
         | 
| 64 | 
            +
                  raw_song_definition = definition
         | 
| 65 | 
            +
                else
         | 
| 66 | 
            +
                  raise SongParseError, "Invalid song input"
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
                
         | 
| 69 | 
            +
                return raw_song_definition
         | 
| 70 | 
            +
              end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
              def split_raw_yaml_into_components(raw_song_definition)
         | 
| 73 | 
            +
                raw_song_components = {}
         | 
| 74 | 
            +
                raw_song_components[:full_definition] = downcase_hash_keys(raw_song_definition)
         | 
| 75 | 
            +
                
         | 
| 76 | 
            +
                if(raw_song_components[:full_definition]["song"] != nil)
         | 
| 77 | 
            +
                  raw_song_components[:header] = downcase_hash_keys(raw_song_components[:full_definition]["song"])
         | 
| 78 | 
            +
                else
         | 
| 79 | 
            +
                  raise SongParseError, NO_SONG_HEADER_ERROR_MSG
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
                raw_song_components[:tempo]     = raw_song_components[:header]["tempo"]
         | 
| 82 | 
            +
                raw_song_components[:kit]       = raw_song_components[:header]["kit"]
         | 
| 83 | 
            +
                raw_song_components[:structure] = raw_song_components[:header]["structure"]
         | 
| 84 | 
            +
                raw_song_components[:patterns]  = raw_song_components[:full_definition].reject {|k, v| k == "song"}
         | 
| 85 | 
            +
              
         | 
| 86 | 
            +
                return raw_song_components
         | 
| 87 | 
            +
              end
         | 
| 88 | 
            +
                  
         | 
| 89 | 
            +
              def build_kit(base_path, raw_kit, raw_patterns)
         | 
| 90 | 
            +
                kit = Kit.new(base_path)
         | 
| 91 | 
            +
                
         | 
| 92 | 
            +
                # Add sounds defined in the Kit section of the song header
         | 
| 93 | 
            +
                if(raw_kit != nil)
         | 
| 94 | 
            +
                  raw_kit.each {|kit_item|
         | 
| 95 | 
            +
                    kit.add(kit_item.keys.first, kit_item.values.first)
         | 
| 96 | 
            +
                  }
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
                
         | 
| 99 | 
            +
                # Add sounds not defined in Kit section, but used in individual tracks
         | 
| 100 | 
            +
                # TODO Investigate detecting duplicate keys already defined in the Kit section, as this could possibly
         | 
| 101 | 
            +
                # result in a performance improvement when the sound has to be converted to a different bit rate/num channels,
         | 
| 102 | 
            +
                # as well as use less memory.
         | 
| 103 | 
            +
                raw_patterns.keys.each{|key|
         | 
| 104 | 
            +
                  track_list = raw_patterns[key]
         | 
| 105 | 
            +
                  track_list.each{|track_definition|
         | 
| 106 | 
            +
                    track_name = track_definition.keys.first
         | 
| 107 | 
            +
                    track_path = track_name
         | 
| 108 | 
            +
                    
         | 
| 109 | 
            +
                    kit.add(track_name, track_path)
         | 
| 110 | 
            +
                  }
         | 
| 111 | 
            +
                }
         | 
| 112 | 
            +
                
         | 
| 113 | 
            +
                return kit
         | 
| 114 | 
            +
              end
         | 
| 115 | 
            +
              
         | 
| 116 | 
            +
              def add_patterns_to_song(song, raw_patterns)
         | 
| 117 | 
            +
                raw_patterns.keys.each{|key|
         | 
| 118 | 
            +
                  new_pattern = song.pattern key.to_sym
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  track_list = raw_patterns[key]
         | 
| 121 | 
            +
                  track_list.each{|track_definition|
         | 
| 122 | 
            +
                    track_name = track_definition.keys.first
         | 
| 123 | 
            +
                   new_pattern.track track_name, song.kit.get_sample_data(track_name), track_definition[track_name]
         | 
| 124 | 
            +
                  }
         | 
| 125 | 
            +
                }
         | 
| 126 | 
            +
              end
         | 
| 127 | 
            +
              
         | 
| 128 | 
            +
              def set_song_structure(song, raw_structure)
         | 
| 129 | 
            +
                structure = []
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                raw_structure.each{|pattern_item|
         | 
| 132 | 
            +
                  if(pattern_item.class == String)
         | 
| 133 | 
            +
                    pattern_item = {pattern_item => "x1"}
         | 
| 134 | 
            +
                  end
         | 
| 135 | 
            +
                  
         | 
| 136 | 
            +
                  pattern_name = pattern_item.keys.first
         | 
| 137 | 
            +
                  pattern_name_sym = pattern_name.downcase.to_sym
         | 
| 138 | 
            +
                  
         | 
| 139 | 
            +
                  # Convert the number of repeats from a String such as "x4" into an integer such as 4.
         | 
| 140 | 
            +
                  multiples_str = pattern_item[pattern_name]
         | 
| 141 | 
            +
                  multiples_str.slice!(0)
         | 
| 142 | 
            +
                  multiples = multiples_str.to_i
         | 
| 143 | 
            +
                  
         | 
| 144 | 
            +
                  if(multiples_str.match(/[^0-9]/) != nil)
         | 
| 145 | 
            +
                    raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Number of repeats should be a whole number."
         | 
| 146 | 
            +
                  else
         | 
| 147 | 
            +
                    if(multiples < 0)
         | 
| 148 | 
            +
                      raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Must be 0 or greater."
         | 
| 149 | 
            +
                    elsif(multiples > 0 && !song.patterns.has_key?(pattern_name_sym))
         | 
| 150 | 
            +
                      # This test is purposefully designed to only throw an error if the number of repeats is greater
         | 
| 151 | 
            +
                      # than 0. This allows you to specify an undefined pattern in the structure with "x0" repeats.
         | 
| 152 | 
            +
                      # This can be convenient for defining the structure before all patterns have been added to the song file.
         | 
| 153 | 
            +
                      raise SongParseError, "Song structure includes non-existent pattern: #{pattern_name}."
         | 
| 154 | 
            +
                    end
         | 
| 155 | 
            +
                  end
         | 
| 156 | 
            +
                  
         | 
| 157 | 
            +
                  multiples.times { structure << pattern_name_sym }
         | 
| 158 | 
            +
                }
         | 
| 159 | 
            +
                song.structure = structure
         | 
| 160 | 
            +
              end
         | 
| 161 | 
            +
                
         | 
| 162 | 
            +
              # Converts all hash keys to be lowercase
         | 
| 163 | 
            +
              def downcase_hash_keys(hash)
         | 
| 164 | 
            +
                return hash.inject({}) {|new_hash, pair|
         | 
| 165 | 
            +
                    new_hash[pair.first.downcase] = pair.last
         | 
| 166 | 
            +
                    new_hash
         | 
| 167 | 
            +
                }
         | 
| 168 | 
            +
              end
         | 
| 169 | 
            +
            end
         | 
    
        data/test/includes.rb
    CHANGED
    
    
    
        data/test/kit_test.rb
    CHANGED
    
    | @@ -6,50 +6,55 @@ class SongTest < Test::Unit::TestCase | |
| 6 6 | 
             
              MIN_SAMPLE_8BIT = 0
         | 
| 7 7 | 
             
              MAX_SAMPLE_8BIT = 255
         | 
| 8 8 |  | 
| 9 | 
            -
              def  | 
| 9 | 
            +
              def test_valid_add
         | 
| 10 10 | 
             
                # Test adding sounds with progressively higher bits per sample and num channels.
         | 
| 11 11 | 
             
                # Verify that kit.bits_per_sample and kit.num_channels is ratcheted up.
         | 
| 12 | 
            -
                kit = Kit.new()
         | 
| 12 | 
            +
                kit = Kit.new("test/sounds")
         | 
| 13 13 | 
             
                assert_equal(kit.bits_per_sample, 0)
         | 
| 14 14 | 
             
                assert_equal(kit.num_channels, 0)
         | 
| 15 15 | 
             
                assert_equal(kit.size, 0)
         | 
| 16 | 
            -
                kit.add("mono8", " | 
| 16 | 
            +
                kit.add("mono8", "bass_mono_8.wav")
         | 
| 17 17 | 
             
                assert_equal(kit.bits_per_sample, 8)
         | 
| 18 18 | 
             
                assert_equal(kit.num_channels, 1)
         | 
| 19 19 | 
             
                assert_equal(kit.size, 1)
         | 
| 20 | 
            -
                kit.add("mono16", " | 
| 20 | 
            +
                kit.add("mono16", "bass_mono_16.wav")
         | 
| 21 21 | 
             
                assert_equal(kit.bits_per_sample, 16)
         | 
| 22 22 | 
             
                assert_equal(kit.num_channels, 1)
         | 
| 23 23 | 
             
                assert_equal(kit.size, 2)
         | 
| 24 | 
            -
                kit.add("stereo16", " | 
| 24 | 
            +
                kit.add("stereo16", "bass_stereo_16.wav")
         | 
| 25 25 | 
             
                assert_equal(kit.bits_per_sample, 16)
         | 
| 26 26 | 
             
                assert_equal(kit.num_channels, 2)
         | 
| 27 27 | 
             
                assert_equal(kit.size, 3)
         | 
| 28 28 |  | 
| 29 29 | 
             
                # Test adding sounds with progressively lower bits per sample and num channels.
         | 
| 30 30 | 
             
                # Verify that kit.bits_per_sample and kit.num_channels doesn't change.
         | 
| 31 | 
            -
                kit = Kit.new()
         | 
| 31 | 
            +
                kit = Kit.new("test/sounds")
         | 
| 32 32 | 
             
                assert_equal(kit.bits_per_sample, 0)
         | 
| 33 33 | 
             
                assert_equal(kit.num_channels, 0)
         | 
| 34 | 
            -
                kit.add("stereo16", " | 
| 34 | 
            +
                kit.add("stereo16", "bass_stereo_16.wav")
         | 
| 35 35 | 
             
                assert_equal(kit.bits_per_sample, 16)
         | 
| 36 36 | 
             
                assert_equal(kit.num_channels, 2)
         | 
| 37 | 
            -
                kit.add("mono16", " | 
| 37 | 
            +
                kit.add("mono16", "bass_mono_16.wav")
         | 
| 38 38 | 
             
                assert_equal(kit.bits_per_sample, 16)
         | 
| 39 39 | 
             
                assert_equal(kit.num_channels, 2)
         | 
| 40 | 
            -
                kit.add("mono8", " | 
| 40 | 
            +
                kit.add("mono8", "bass_mono_8.wav")
         | 
| 41 41 | 
             
                assert_equal(kit.bits_per_sample, 16)
         | 
| 42 42 | 
             
                assert_equal(kit.num_channels, 2)
         | 
| 43 43 | 
             
              end
         | 
| 44 44 |  | 
| 45 | 
            +
              def test_invalid_add
         | 
| 46 | 
            +
                kit = Kit.new("test/sounds")
         | 
| 47 | 
            +
                assert_raise(SoundNotFoundError) { kit.add("i_do_not_exist", "i_do_not_exist.wav") }
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
             | 
| 45 50 | 
             
              def test_get_sample_data
         | 
| 46 | 
            -
                kit = Kit.new()
         | 
| 51 | 
            +
                kit = Kit.new("test/sounds")
         | 
| 47 52 |  | 
| 48 53 | 
             
                assert_raise(StandardError) { kit.get_sample_data("nonexistant") }
         | 
| 49 54 |  | 
| 50 55 | 
             
                # Test adding sounds with progressively higher bits per sample and num channels.
         | 
| 51 56 | 
             
                # Verify that sample data bits per sample and num channels is ratcheted up.
         | 
| 52 | 
            -
                kit.add("mono8", " | 
| 57 | 
            +
                kit.add("mono8", "bass_mono_8.wav")
         | 
| 53 58 | 
             
                sample_data = kit.get_sample_data("mono8")
         | 
| 54 59 | 
             
                assert(sample_data.max <= MAX_SAMPLE_8BIT)
         | 
| 55 60 | 
             
                assert(sample_data.min >= MIN_SAMPLE_8BIT)
         | 
| @@ -59,7 +64,7 @@ class SongTest < Test::Unit::TestCase | |
| 59 64 | 
             
                }
         | 
| 60 65 | 
             
                assert(all_are_fixnums)
         | 
| 61 66 |  | 
| 62 | 
            -
                kit.add("mono16", " | 
| 67 | 
            +
                kit.add("mono16", "bass_mono_16.wav")
         | 
| 63 68 | 
             
                sample_data = kit.get_sample_data("mono8")
         | 
| 64 69 | 
             
                assert(sample_data.max > MAX_SAMPLE_8BIT)
         | 
| 65 70 | 
             
                assert(sample_data.min < MIN_SAMPLE_8BIT)
         | 
| @@ -69,7 +74,7 @@ class SongTest < Test::Unit::TestCase | |
| 69 74 | 
             
                }
         | 
| 70 75 | 
             
                assert(all_are_fixnums)
         | 
| 71 76 |  | 
| 72 | 
            -
                kit.add("stereo16", " | 
| 77 | 
            +
                kit.add("stereo16", "bass_stereo_16.wav")
         | 
| 73 78 | 
             
                sample_data = kit.get_sample_data("stereo16")
         | 
| 74 79 | 
             
                assert(sample_data.flatten.max > MAX_SAMPLE_8BIT)
         | 
| 75 80 | 
             
                assert(sample_data.flatten.min < MIN_SAMPLE_8BIT)
         | 
| @@ -83,9 +88,9 @@ class SongTest < Test::Unit::TestCase | |
| 83 88 |  | 
| 84 89 | 
             
                # Test adding sounds with progressively lower bits per sample and num channels.
         | 
| 85 90 | 
             
                # Verify that sample data bits per sample and num channels doesn't go down.
         | 
| 86 | 
            -
                kit = Kit.new()
         | 
| 91 | 
            +
                kit = Kit.new("test/sounds")
         | 
| 87 92 |  | 
| 88 | 
            -
                kit.add("stereo16", " | 
| 93 | 
            +
                kit.add("stereo16", "bass_stereo_16.wav")
         | 
| 89 94 | 
             
                sample_data = kit.get_sample_data("stereo16")
         | 
| 90 95 | 
             
                assert(sample_data.flatten.max > MAX_SAMPLE_8BIT)
         | 
| 91 96 | 
             
                assert(sample_data.flatten.min < MIN_SAMPLE_8BIT)
         | 
| @@ -96,7 +101,7 @@ class SongTest < Test::Unit::TestCase | |
| 96 101 | 
             
                assert(all_are_arrays)
         | 
| 97 102 | 
             
                assert(sample_data.first.length == 2)
         | 
| 98 103 |  | 
| 99 | 
            -
                kit.add("mono16", " | 
| 104 | 
            +
                kit.add("mono16", "bass_mono_16.wav")
         | 
| 100 105 | 
             
                sample_data = kit.get_sample_data("mono16")
         | 
| 101 106 | 
             
                assert(sample_data.flatten.max > MAX_SAMPLE_8BIT)
         | 
| 102 107 | 
             
                assert(sample_data.flatten.min < MIN_SAMPLE_8BIT)
         | 
| @@ -107,7 +112,7 @@ class SongTest < Test::Unit::TestCase | |
| 107 112 | 
             
                assert(all_are_arrays)
         | 
| 108 113 | 
             
                assert(sample_data.first.length == 2)
         | 
| 109 114 |  | 
| 110 | 
            -
                kit.add("mono8", " | 
| 115 | 
            +
                kit.add("mono8", "bass_mono_8.wav")
         | 
| 111 116 | 
             
                sample_data = kit.get_sample_data("mono8")
         | 
| 112 117 | 
             
                assert(sample_data.flatten.max > MAX_SAMPLE_8BIT)
         | 
| 113 118 | 
             
                assert(sample_data.flatten.min < MIN_SAMPLE_8BIT)
         | 
    
        data/test/pattern_test.rb
    CHANGED
    
    | @@ -7,11 +7,11 @@ class PatternTest < Test::Unit::TestCase | |
| 7 7 | 
             
              SECONDS_IN_MINUTE = 60.0
         | 
| 8 8 |  | 
| 9 9 | 
             
              def generate_test_data
         | 
| 10 | 
            -
                kit = Kit.new()
         | 
| 11 | 
            -
                kit.add("bass.wav",      " | 
| 12 | 
            -
                kit.add("snare.wav",     " | 
| 13 | 
            -
                kit.add("hh_closed.wav", " | 
| 14 | 
            -
                kit.add("hh_open.wav",   " | 
| 10 | 
            +
                kit = Kit.new("test/sounds")
         | 
| 11 | 
            +
                kit.add("bass.wav",      "bass_mono_8.wav")
         | 
| 12 | 
            +
                kit.add("snare.wav",     "snare_mono_8.wav")
         | 
| 13 | 
            +
                kit.add("hh_closed.wav", "hh_closed_mono_8.wav")
         | 
| 14 | 
            +
                kit.add("hh_open.wav",   "hh_open_mono_8.wav")
         | 
| 15 15 |  | 
| 16 16 | 
             
                test_patterns = []
         | 
| 17 17 |  | 
    
        data/test/song_test.rb
    CHANGED
    
    | @@ -2,41 +2,26 @@ $:.unshift File.join(File.dirname(__FILE__),'..','lib') | |
| 2 2 |  | 
| 3 3 | 
             
            require 'test/includes'
         | 
| 4 4 |  | 
| 5 | 
            -
            class MockSong < Song
         | 
| 6 | 
            -
              attr_reader :patterns
         | 
| 7 | 
            -
              attr_accessor :kit
         | 
| 8 | 
            -
            end
         | 
| 9 | 
            -
             | 
| 10 5 | 
             
            class SongTest < Test::Unit::TestCase
         | 
| 11 6 | 
             
              DEFAULT_TEMPO = 120
         | 
| 12 7 |  | 
| 13 8 | 
             
              def generate_test_data
         | 
| 14 | 
            -
                kit = Kit.new()
         | 
| 15 | 
            -
                kit.add("bass.wav",      " | 
| 16 | 
            -
                kit.add("snare.wav",     " | 
| 17 | 
            -
                kit.add("hh_closed.wav", " | 
| 18 | 
            -
                kit.add("ride.wav",      " | 
| 9 | 
            +
                kit = Kit.new("test/sounds")
         | 
| 10 | 
            +
                kit.add("bass.wav",      "bass_mono_8.wav")
         | 
| 11 | 
            +
                kit.add("snare.wav",     "snare_mono_8.wav")
         | 
| 12 | 
            +
                kit.add("hh_closed.wav", "hh_closed_mono_8.wav")
         | 
| 13 | 
            +
                kit.add("ride.wav",      "ride_mono_8.wav")
         | 
| 19 14 |  | 
| 20 | 
            -
                test_songs =  | 
| 15 | 
            +
                test_songs = SongParserTest.generate_test_data()
         | 
| 21 16 |  | 
| 22 | 
            -
                test_songs[:blank] =  | 
| 17 | 
            +
                test_songs[:blank] = Song.new("test/sounds")
         | 
| 23 18 |  | 
| 24 | 
            -
                test_songs[:no_structure] =  | 
| 19 | 
            +
                test_songs[:no_structure] = Song.new("test/sounds")
         | 
| 25 20 | 
             
                verse = test_songs[:no_structure].pattern :verse
         | 
| 26 21 | 
             
                verse.track "bass.wav",      kit.get_sample_data("bass.wav"),      "X.......X......."
         | 
| 27 22 | 
             
                verse.track "snare.wav",     kit.get_sample_data("snare.wav"),     "....X.......X..."
         | 
| 28 23 | 
             
                verse.track "hh_closed.wav", kit.get_sample_data("hh_closed.wav"), "X.X.X.X.X.X.X.X."
         | 
| 29 24 |  | 
| 30 | 
            -
                repeats_not_specified_yaml = "
         | 
| 31 | 
            -
            Song:
         | 
| 32 | 
            -
              Tempo: 100
         | 
| 33 | 
            -
              Structure:
         | 
| 34 | 
            -
                - Verse
         | 
| 35 | 
            -
                
         | 
| 36 | 
            -
            Verse:
         | 
| 37 | 
            -
              - test/sounds/bass_mono_8.wav: X"
         | 
| 38 | 
            -
                test_songs[:repeats_not_specified] = MockSong.new(File.dirname(__FILE__) + "/..", repeats_not_specified_yaml)
         | 
| 39 | 
            -
                
         | 
| 40 25 | 
             
                overflow_yaml = "
         | 
| 41 26 | 
             
            Song:
         | 
| 42 27 | 
             
              Tempo: 100
         | 
| @@ -45,9 +30,9 @@ Song: | |
| 45 30 |  | 
| 46 31 | 
             
            Verse:
         | 
| 47 32 | 
             
              - test/sounds/snare_mono_8.wav: ...X"
         | 
| 48 | 
            -
                test_songs[:overflow] =  | 
| 33 | 
            +
                test_songs[:overflow] = SongParser.new().parse(File.dirname(__FILE__) + "/..", overflow_yaml)
         | 
| 49 34 |  | 
| 50 | 
            -
                test_songs[:from_code] =  | 
| 35 | 
            +
                test_songs[:from_code] = Song.new("test/sounds")
         | 
| 51 36 | 
             
                verse = test_songs[:from_code].pattern :verse
         | 
| 52 37 | 
             
                verse.track "bass.wav",      kit.get_sample_data("bass.wav"),      "X.......X......."
         | 
| 53 38 | 
             
                verse.track "snare.wav",     kit.get_sample_data("snare.wav"),     "....X.......X..."
         | 
| @@ -59,91 +44,20 @@ Verse: | |
| 59 44 | 
             
                test_songs[:from_code].structure = [:verse, :chorus, :verse, :chorus, :chorus]
         | 
| 60 45 | 
             
                test_songs[:from_code].kit = kit
         | 
| 61 46 |  | 
| 62 | 
            -
                valid_yaml_string = "# An example song
         | 
| 63 | 
            -
             | 
| 64 | 
            -
            Song:
         | 
| 65 | 
            -
              Tempo: 99
         | 
| 66 | 
            -
              Structure:
         | 
| 67 | 
            -
                - Verse:  x2
         | 
| 68 | 
            -
                - Chorus: x2
         | 
| 69 | 
            -
                - Verse:  x2
         | 
| 70 | 
            -
                - Chorus: x4
         | 
| 71 | 
            -
                - Bridge: x1
         | 
| 72 | 
            -
                - Chorus: x4
         | 
| 73 | 
            -
             | 
| 74 | 
            -
            Verse:
         | 
| 75 | 
            -
              - test/sounds/bass_mono_8.wav:      X...X...X...XX..X...X...XX..X...
         | 
| 76 | 
            -
              - test/sounds/snare_mono_8.wav:     ..X...X...X...X.X...X...X...X...
         | 
| 77 | 
            -
            # Here is a comment
         | 
| 78 | 
            -
              - test/sounds/hh_closed_mono_8.wav: X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.
         | 
| 79 | 
            -
              - test/sounds/hh_open_mono_8.wav:   X...............X..............X
         | 
| 80 | 
            -
            # Here is another comment
         | 
| 81 | 
            -
            Chorus:
         | 
| 82 | 
            -
              - test/sounds/bass_mono_8.wav:      X...X...XXXXXXXXX...X...X...X...
         | 
| 83 | 
            -
              - test/sounds/snare_mono_8.wav:     ...................X...X...X...X
         | 
| 84 | 
            -
              - test/sounds/hh_closed_mono_8.wav: X.X.XXX.X.X.XXX.X.X.XXX.X.X.XXX. # It's comment time
         | 
| 85 | 
            -
              - test/sounds/hh_open_mono_8.wav:   ........X.......X.......X.......
         | 
| 86 | 
            -
              - test/sounds/ride_mono_8.wav:      ....X...................X.......
         | 
| 87 | 
            -
             | 
| 88 | 
            -
             | 
| 89 | 
            -
            Bridge:
         | 
| 90 | 
            -
              - test/sounds/hh_closed_mono_8.wav: XX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.X"
         | 
| 91 | 
            -
                
         | 
| 92 | 
            -
                test_songs[:from_valid_yaml_string] = MockSong.new(File.dirname(__FILE__) + "/..", valid_yaml_string)
         | 
| 93 | 
            -
                
         | 
| 94 47 | 
             
                return test_songs
         | 
| 95 48 | 
             
              end
         | 
| 96 49 |  | 
| 97 50 | 
             
              def test_initialize
         | 
| 98 | 
            -
                test_songs = generate_test_data
         | 
| 51 | 
            +
                test_songs = generate_test_data()
         | 
| 99 52 |  | 
| 100 53 | 
             
                assert_equal(test_songs[:blank].structure, [])
         | 
| 101 54 | 
             
                assert_equal(test_songs[:blank].tick_sample_length, (Song::SAMPLE_RATE * Song::SECONDS_PER_MINUTE) / DEFAULT_TEMPO / 4.0)
         | 
| 55 | 
            +
                
         | 
| 102 56 | 
             
                assert_equal(test_songs[:no_structure].structure, [])
         | 
| 103 57 | 
             
                assert_equal(test_songs[:no_structure].tick_sample_length, (Song::SAMPLE_RATE * Song::SECONDS_PER_MINUTE) / DEFAULT_TEMPO / 4.0)
         | 
| 58 | 
            +
                
         | 
| 104 59 | 
             
                assert_equal(test_songs[:from_code].structure, [:verse, :chorus, :verse, :chorus, :chorus])
         | 
| 105 60 | 
             
                assert_equal(test_songs[:from_code].tick_sample_length, (Song::SAMPLE_RATE * Song::SECONDS_PER_MINUTE) / DEFAULT_TEMPO / 4.0)
         | 
| 106 | 
            -
                
         | 
| 107 | 
            -
                assert_equal(test_songs[:from_valid_yaml_string].structure, [:verse, :verse, :chorus, :chorus, :verse, :verse, :chorus, :chorus, :chorus, :chorus, :bridge, :chorus, :chorus, :chorus, :chorus])
         | 
| 108 | 
            -
                assert_equal(test_songs[:from_valid_yaml_string].tempo, 99)
         | 
| 109 | 
            -
                assert_equal(test_songs[:from_valid_yaml_string].tick_sample_length, (Song::SAMPLE_RATE * Song::SECONDS_PER_MINUTE) / 99 / 4.0)
         | 
| 110 | 
            -
                assert_equal(test_songs[:from_valid_yaml_string].patterns.keys.map{|key| key.to_s}.sort, ["bridge", "chorus", "verse"])
         | 
| 111 | 
            -
                assert_equal(test_songs[:from_valid_yaml_string].patterns[:verse].tracks.length, 4)
         | 
| 112 | 
            -
                assert_equal(test_songs[:from_valid_yaml_string].patterns[:chorus].tracks.length, 5)
         | 
| 113 | 
            -
                assert_equal(test_songs[:from_valid_yaml_string].patterns[:bridge].tracks.length, 1)
         | 
| 114 | 
            -
              end
         | 
| 115 | 
            -
              
         | 
| 116 | 
            -
              def test_invalid_initialize
         | 
| 117 | 
            -
                invalid_tempo_yaml_string = "# Invalid tempo song
         | 
| 118 | 
            -
                Song:
         | 
| 119 | 
            -
                  Tempo: 100a
         | 
| 120 | 
            -
                  Structure:
         | 
| 121 | 
            -
                    - Verse:  x2
         | 
| 122 | 
            -
             | 
| 123 | 
            -
                Verse:
         | 
| 124 | 
            -
                  - test/sounds/bass_mono_8.wav:      X...X...X...XX..X...X...XX..X..."
         | 
| 125 | 
            -
                assert_raise(SongParseError) { song = MockSong.new(File.dirname(__FILE__) + "/..", invalid_tempo_yaml_string) }
         | 
| 126 | 
            -
                
         | 
| 127 | 
            -
                invalid_structure_yaml_string = "# Invalid structure song
         | 
| 128 | 
            -
                Song:
         | 
| 129 | 
            -
                  Tempo: 100
         | 
| 130 | 
            -
                  Structure:
         | 
| 131 | 
            -
                    - Verse:  x2
         | 
| 132 | 
            -
                    - Chorus: x1
         | 
| 133 | 
            -
             | 
| 134 | 
            -
                Verse:
         | 
| 135 | 
            -
                  - test/sounds/bass_mono_8.wav:      X...X...X...XX..X...X...XX..X..."
         | 
| 136 | 
            -
                assert_raise(SongParseError) { song = MockSong.new(File.dirname(__FILE__) + "/..", invalid_structure_yaml_string) }
         | 
| 137 | 
            -
                
         | 
| 138 | 
            -
                invalid_repeats_yaml_string = "    # Invalid structure song
         | 
| 139 | 
            -
                Song:
         | 
| 140 | 
            -
                  Tempo: 100
         | 
| 141 | 
            -
                  Structure:
         | 
| 142 | 
            -
                    - Verse:  x2a
         | 
| 143 | 
            -
             | 
| 144 | 
            -
                Verse:
         | 
| 145 | 
            -
                  - test/sounds/bass_mono_8.wav:      X...X...X...XX..X...X...XX..X..."
         | 
| 146 | 
            -
                assert_raise(SongParseError) { song = MockSong.new(File.dirname(__FILE__) + "/..", invalid_repeats_yaml_string) }
         | 
| 147 61 | 
             
              end
         | 
| 148 62 |  | 
| 149 63 | 
             
              def test_total_tracks
         | 
| @@ -0,0 +1,207 @@ | |
| 1 | 
            +
            $:.unshift File.join(File.dirname(__FILE__),'..','lib')
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'test/includes'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            class SongParserTest < Test::Unit::TestCase
         | 
| 6 | 
            +
              def self.generate_test_data
         | 
| 7 | 
            +
                kit = Kit.new("test/sounds")
         | 
| 8 | 
            +
                kit.add("bass.wav",      "bass_mono_8.wav")
         | 
| 9 | 
            +
                kit.add("snare.wav",     "snare_mono_8.wav")
         | 
| 10 | 
            +
                kit.add("hh_closed.wav", "hh_closed_mono_8.wav")
         | 
| 11 | 
            +
                kit.add("ride.wav",      "ride_mono_8.wav")
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                test_songs = {}
         | 
| 14 | 
            +
                base_path = File.dirname(__FILE__) + "/.."
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                no_tempo_yaml = "
         | 
| 17 | 
            +
            Song:
         | 
| 18 | 
            +
              Structure:
         | 
| 19 | 
            +
                - Verse: x1
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            Verse:
         | 
| 22 | 
            +
              - test/sounds/bass_mono_8.wav: X"
         | 
| 23 | 
            +
                test_songs[:no_tempo] = SongParser.new().parse(base_path, no_tempo_yaml)
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                repeats_not_specified_yaml = "
         | 
| 26 | 
            +
            Song:
         | 
| 27 | 
            +
              Tempo: 100
         | 
| 28 | 
            +
              Structure:
         | 
| 29 | 
            +
                - Verse
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            Verse:
         | 
| 32 | 
            +
              - test/sounds/bass_mono_8.wav: X"
         | 
| 33 | 
            +
                test_songs[:repeats_not_specified] = SongParser.new().parse(base_path, repeats_not_specified_yaml)
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                overflow_yaml = "
         | 
| 36 | 
            +
            Song:
         | 
| 37 | 
            +
              Tempo: 100
         | 
| 38 | 
            +
              Structure:
         | 
| 39 | 
            +
                - Verse: x2
         | 
| 40 | 
            +
             | 
| 41 | 
            +
            Verse:
         | 
| 42 | 
            +
              - test/sounds/snare_mono_8.wav: ...X"
         | 
| 43 | 
            +
                test_songs[:overflow] = SongParser.new().parse(base_path, overflow_yaml)
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                valid_yaml_string = "# An example song
         | 
| 46 | 
            +
              
         | 
| 47 | 
            +
            Song:
         | 
| 48 | 
            +
              Tempo: 99
         | 
| 49 | 
            +
              Structure:
         | 
| 50 | 
            +
                - Verse:     x2
         | 
| 51 | 
            +
                - Chorus:    x2
         | 
| 52 | 
            +
                - Verse:     x2
         | 
| 53 | 
            +
                - Chorus:    x4
         | 
| 54 | 
            +
                - Bridge:    x1
         | 
| 55 | 
            +
                - Undefined: x0  # This is legal as long as num repeats is 0.
         | 
| 56 | 
            +
                - Chorus:    x4
         | 
| 57 | 
            +
             | 
| 58 | 
            +
            Verse:
         | 
| 59 | 
            +
              - test/sounds/bass_mono_8.wav:      X...X...X...XX..X...X...XX..X...
         | 
| 60 | 
            +
              - test/sounds/snare_mono_8.wav:     ..X...X...X...X.X...X...X...X...
         | 
| 61 | 
            +
            # Here is a comment
         | 
| 62 | 
            +
              - test/sounds/hh_closed_mono_8.wav: X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.
         | 
| 63 | 
            +
              - test/sounds/hh_open_mono_8.wav:   X...............X..............X
         | 
| 64 | 
            +
            # Here is another comment
         | 
| 65 | 
            +
            Chorus:
         | 
| 66 | 
            +
              - test/sounds/bass_mono_8.wav:      X...X...XXXXXXXXX...X...X...X...
         | 
| 67 | 
            +
              - test/sounds/snare_mono_8.wav:     ...................X...X...X...X
         | 
| 68 | 
            +
              - test/sounds/hh_closed_mono_8.wav: X.X.XXX.X.X.XXX.X.X.XXX.X.X.XXX. # It's comment time
         | 
| 69 | 
            +
              - test/sounds/hh_open_mono_8.wav:   ........X.......X.......X.......
         | 
| 70 | 
            +
              - test/sounds/ride_mono_8.wav:      ....X...................X.......
         | 
| 71 | 
            +
             | 
| 72 | 
            +
             | 
| 73 | 
            +
            Bridge:
         | 
| 74 | 
            +
              - test/sounds/hh_closed_mono_8.wav: XX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.X"
         | 
| 75 | 
            +
                test_songs[:from_valid_yaml_string] = SongParser.new().parse(base_path, valid_yaml_string)
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                valid_yaml_string_with_kit = "# An example song
         | 
| 78 | 
            +
             | 
| 79 | 
            +
            Song:
         | 
| 80 | 
            +
              Tempo: 99
         | 
| 81 | 
            +
              Kit:
         | 
| 82 | 
            +
                - bass:     test/sounds/bass_mono_8.wav
         | 
| 83 | 
            +
                - snare:    test/sounds/snare_mono_8.wav
         | 
| 84 | 
            +
                - hhclosed: test/sounds/hh_closed_mono_8.wav
         | 
| 85 | 
            +
                - hhopen:   test/sounds/hh_open_mono_8.wav
         | 
| 86 | 
            +
              Structure:
         | 
| 87 | 
            +
                - Verse:  x2
         | 
| 88 | 
            +
                - Chorus: x2
         | 
| 89 | 
            +
                - Verse:  x2
         | 
| 90 | 
            +
                - Chorus: x4
         | 
| 91 | 
            +
                - Bridge: x1
         | 
| 92 | 
            +
                - Undefined: x0  # This is legal as long as num repeats is 0.
         | 
| 93 | 
            +
                - Chorus: x4
         | 
| 94 | 
            +
             | 
| 95 | 
            +
            Verse:
         | 
| 96 | 
            +
              - base:      X...X...X...XX..X...X...XX..X...
         | 
| 97 | 
            +
              - snare:     ..X...X...X...X.X...X...X...X...
         | 
| 98 | 
            +
            # Here is a comment
         | 
| 99 | 
            +
              - hhclosed:  X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.
         | 
| 100 | 
            +
              - hhopen:    X...............X..............X
         | 
| 101 | 
            +
            # Here is another comment
         | 
| 102 | 
            +
            Chorus:
         | 
| 103 | 
            +
              - bass:      X...X...XXXXXXXXX...X...X...X...
         | 
| 104 | 
            +
              - snare:     ...................X...X...X...X
         | 
| 105 | 
            +
              - test/sounds/hh_closed_mono_8.wav: X.X.XXX.X.X.XXX.X.X.XXX.X.X.XXX. # It's comment time
         | 
| 106 | 
            +
              - hhopen:    ........X.......X.......X.......
         | 
| 107 | 
            +
              - test/sounds/ride_mono_8.wav:      ....X...................X.......
         | 
| 108 | 
            +
             | 
| 109 | 
            +
            Bridge:
         | 
| 110 | 
            +
              - hhclosed: XX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.X"
         | 
| 111 | 
            +
                test_songs[:from_valid_yaml_string_with_kit] = SongParser.new().parse(base_path, valid_yaml_string)
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                return test_songs
         | 
| 114 | 
            +
              end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
              def test_valid_initialize
         | 
| 117 | 
            +
                test_songs = SongParserTest.generate_test_data()
         | 
| 118 | 
            +
                
         | 
| 119 | 
            +
                assert_equal(test_songs[:no_tempo].tempo, 120)
         | 
| 120 | 
            +
                assert_equal(test_songs[:no_tempo].structure, [:verse])
         | 
| 121 | 
            +
                
         | 
| 122 | 
            +
                assert_equal(test_songs[:repeats_not_specified].tempo, 100)
         | 
| 123 | 
            +
                assert_equal(test_songs[:repeats_not_specified].structure, [:verse])
         | 
| 124 | 
            +
                
         | 
| 125 | 
            +
                # These two songs should be the same, except that one uses a kit in the song header
         | 
| 126 | 
            +
                # and the other doesn't.
         | 
| 127 | 
            +
                [:from_valid_yaml_string, :from_valid_yaml_string_with_kit].each {|song_key|
         | 
| 128 | 
            +
                  song = test_songs[song_key]
         | 
| 129 | 
            +
                  assert_equal(song.structure, [:verse, :verse, :chorus, :chorus, :verse, :verse, :chorus, :chorus, :chorus, :chorus, :bridge, :chorus, :chorus, :chorus, :chorus])
         | 
| 130 | 
            +
                  assert_equal(song.tempo, 99)
         | 
| 131 | 
            +
                  assert_equal(song.tick_sample_length, (Song::SAMPLE_RATE * Song::SECONDS_PER_MINUTE) / 99 / 4.0)
         | 
| 132 | 
            +
                  assert_equal(song.patterns.keys.map{|key| key.to_s}.sort, ["bridge", "chorus", "verse"])
         | 
| 133 | 
            +
                  assert_equal(song.patterns[:verse].tracks.length, 4)
         | 
| 134 | 
            +
                  assert_equal(song.patterns[:chorus].tracks.length, 5)
         | 
| 135 | 
            +
                  assert_equal(song.patterns[:bridge].tracks.length, 1)
         | 
| 136 | 
            +
                }
         | 
| 137 | 
            +
              end
         | 
| 138 | 
            +
              
         | 
| 139 | 
            +
              def test_invalid_initialize
         | 
| 140 | 
            +
                no_header_yaml_string = "# Song with no header
         | 
| 141 | 
            +
                Verse:
         | 
| 142 | 
            +
                  - test/sounds/bass_mono_8.wav:      X...X...X...XX..X...X...XX..X..."
         | 
| 143 | 
            +
                assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", no_header_yaml_string) }
         | 
| 144 | 
            +
                
         | 
| 145 | 
            +
                sound_doesnt_exist_yaml_string = "# Song with non-existent sound
         | 
| 146 | 
            +
                Song:
         | 
| 147 | 
            +
                  Tempo: 100
         | 
| 148 | 
            +
                  Structure:
         | 
| 149 | 
            +
                    - Verse: x1
         | 
| 150 | 
            +
                    
         | 
| 151 | 
            +
                Verse:
         | 
| 152 | 
            +
                  - test/sounds/i_do_not_exist.wav: X...X..."
         | 
| 153 | 
            +
                assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", sound_doesnt_exist_yaml_string) }
         | 
| 154 | 
            +
                
         | 
| 155 | 
            +
                
         | 
| 156 | 
            +
                sound_doesnt_exist_in_kit_yaml_string = "# Song with non-existent sound in Kit
         | 
| 157 | 
            +
                Song:
         | 
| 158 | 
            +
                  Tempo: 100
         | 
| 159 | 
            +
                  Structure:
         | 
| 160 | 
            +
                    - Verse: x1
         | 
| 161 | 
            +
                  Kit:
         | 
| 162 | 
            +
                    - bad: test/sounds/i_do_not_exist.wav
         | 
| 163 | 
            +
                  
         | 
| 164 | 
            +
                Verse:
         | 
| 165 | 
            +
                  - bad: X...X..."
         | 
| 166 | 
            +
                assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", sound_doesnt_exist_in_kit_yaml_string) }
         | 
| 167 | 
            +
                
         | 
| 168 | 
            +
                invalid_tempo_yaml_string = "# Song with invalid tempo
         | 
| 169 | 
            +
                Song:
         | 
| 170 | 
            +
                  Tempo: 100a
         | 
| 171 | 
            +
                  Structure:
         | 
| 172 | 
            +
                    - Verse:  x2
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                Verse:
         | 
| 175 | 
            +
                  - test/sounds/bass_mono_8.wav:      X...X...X...XX..X...X...XX..X..."
         | 
| 176 | 
            +
                assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", invalid_tempo_yaml_string) }
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                invalid_structure_yaml_string = "# Song whose structure references non-existent pattern
         | 
| 179 | 
            +
                Song:
         | 
| 180 | 
            +
                  Tempo: 100
         | 
| 181 | 
            +
                  Structure:
         | 
| 182 | 
            +
                    - Verse:  x2
         | 
| 183 | 
            +
                    - Chorus: x1
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                Verse:
         | 
| 186 | 
            +
                  - test/sounds/bass_mono_8.wav:      X...X...X...XX..X...X...XX..X..."
         | 
| 187 | 
            +
                assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", invalid_structure_yaml_string) }
         | 
| 188 | 
            +
                
         | 
| 189 | 
            +
                no_structure_yaml_string = "# Song without a structure section in the header
         | 
| 190 | 
            +
                Song:
         | 
| 191 | 
            +
                  Tempo: 100
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                Verse:
         | 
| 194 | 
            +
                  - test/sounds/bass_mono_8.wav:      X...X...X...XX..X...X...XX..X..."
         | 
| 195 | 
            +
                assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", no_structure_yaml_string) }
         | 
| 196 | 
            +
                
         | 
| 197 | 
            +
                invalid_repeats_yaml_string = "# Song with invalid number of repeats for pattern
         | 
| 198 | 
            +
                Song:
         | 
| 199 | 
            +
                  Tempo: 100
         | 
| 200 | 
            +
                  Structure:
         | 
| 201 | 
            +
                    - Verse:  x2a
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                Verse:
         | 
| 204 | 
            +
                  - test/sounds/bass_mono_8.wav:      X...X...X...XX..X...X...XX..X..."
         | 
| 205 | 
            +
                assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", invalid_repeats_yaml_string) }
         | 
| 206 | 
            +
              end
         | 
| 207 | 
            +
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification 
         | 
| 2 2 | 
             
            name: beats
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version 
         | 
| 4 | 
            -
              version: 1. | 
| 4 | 
            +
              version: 1.1.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors: 
         | 
| 7 7 | 
             
            - Joel Strait
         | 
| @@ -9,7 +9,7 @@ autorequire: | |
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 11 |  | 
| 12 | 
            -
            date: 2010- | 
| 12 | 
            +
            date: 2010-04-12 00:00:00 -04:00
         | 
| 13 13 | 
             
            default_executable: 
         | 
| 14 14 | 
             
            dependencies: 
         | 
| 15 15 | 
             
            - !ruby/object:Gem::Dependency 
         | 
| @@ -36,12 +36,14 @@ files: | |
| 36 36 | 
             
            - lib/kit.rb
         | 
| 37 37 | 
             
            - lib/pattern.rb
         | 
| 38 38 | 
             
            - lib/song.rb
         | 
| 39 | 
            +
            - lib/songparser.rb
         | 
| 39 40 | 
             
            - lib/track.rb
         | 
| 40 41 | 
             
            - bin/beats
         | 
| 41 42 | 
             
            - test/includes.rb
         | 
| 42 43 | 
             
            - test/kit_test.rb
         | 
| 43 44 | 
             
            - test/pattern_test.rb
         | 
| 44 45 | 
             
            - test/song_test.rb
         | 
| 46 | 
            +
            - test/songparser_test.rb
         | 
| 45 47 | 
             
            - test/sounds/bass_mono_16.wav
         | 
| 46 48 | 
             
            - test/sounds/bass_mono_8.wav
         | 
| 47 49 | 
             
            - test/sounds/bass_stereo_16.wav
         | 
| @@ -83,6 +85,7 @@ test_files: | |
| 83 85 | 
             
            - test/kit_test.rb
         | 
| 84 86 | 
             
            - test/pattern_test.rb
         | 
| 85 87 | 
             
            - test/song_test.rb
         | 
| 88 | 
            +
            - test/songparser_test.rb
         | 
| 86 89 | 
             
            - test/sounds/bass_mono_16.wav
         | 
| 87 90 | 
             
            - test/sounds/bass_mono_8.wav
         | 
| 88 91 | 
             
            - test/sounds/bass_stereo_16.wav
         |