DEV Community

Kevin Murphy
Kevin Murphy

Posted on • Edited on

Programming Guitar Greatness

I use heavy strings, tune low, play hard, and floor it. Floor it. That's technical talk.
-- Stevie Ray Vaughan

Stevie Ray Vaughan is one of my favorite guitarists. Unfortunately, I can't play anything like he can. To make up for it, let's teach a computer to play guitar like him and see what we can learn.

Hit play for some background music or inspiration, and let's get started.

Use Heavy Strings

Guitars have many strings (typically six) that you manipulate to make different sounds. To build a system to play guitar, it needs to know about strings. To avoid any potential confusion, we'll build a GuitarString class.

Different strings on a guitar are different thicknesses. Thicker strings play notes at a lower frequency than thinner strings. We measure this string thickness in thousands of an inch.

A set of standard strings is a set of nines. The thinnest string in the set is 0.009 inches thick. Stevie Ray Vaughan played a set of 13s that were even thicker on the low end than a stock set of 13s.

These strings are hard to bend, hard to move, and hard to play with. Stevie called them "heavy". We'll know if a string is heavy by comparing it to a standard set.

class GuitarString
  def heavy?
    gauge_number > common_gauge_number
  end
end
Enter fullscreen mode Exit fullscreen mode

Use Domain Terms

We taught our computer about guitar strings using the language of a guitarist. I would typically refer to a wire as having a particular thickness. Stevie referred to his strings as being "heavy". To represent his description, we'll use words that resonate in the world our system models. Our guitar strings are "heavy", not "thick".

Tune Low

We tune each string on a guitar to a note. The most common combination of string tunings is standard tuning. Stevie didn't play in standard tuning. He tuned down a half step. Each string is tuned to a slightly lower pitch than you'd regularly expect.

To support this, we need to be able to to tune the guitar. We need to be able to tune the guitar to standard tuning, and down a half step. We'll accept which tuning to use for our guitar as an argument to a tune method. We'll switch on that argument to implement that tuning.

class Guitar
  def tune(tuning = :standard)
    case tuning
    when :standard
      standard_tuning
    when :down_half_step
      down_half_step_tuning
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

These aren't the only tunings possible for a guitar. There are a lot of them. Supporting more will mean this method gets more complex. Our guitar class gets more complex.

class Guitar
  def tune(tuning = :standard); end
  def standard_tuning; end
  def down_half_step_tuning; end
  def drop_d_tuning; end
  def open_a_tuning; end
  def modal_c_tuning; end
  ...
end
Enter fullscreen mode Exit fullscreen mode

As we handle more and more tunings, our guitar grows in complexity. It starts to look like an instrument primarily responsible for tuning itself. The number of methods that have to do with tuning takes away from the more exciting things you can do with a guitar.

To focus on a guitar's other responsibilities, let's extract this logic. A Tuner class will take a guitar as a dependency and know how to tune it to a variety of tunings. That changes the implementation in our Guitar class to look like this:

class Guitar
  def tune(tuning = :standard)
    Tuner.new(self).tune(tuning)
  end
end
Enter fullscreen mode Exit fullscreen mode

All the complexity of different guitar tunings is still in our system. It's in the Tuner class now, not Guitar.

Extract Related Behavior

In a system responsible for playing the guitar, that class attracts a lot of behavior. That can make understanding all the responsibilities that the class has difficult. When we can identify a set of related behaviors, we should explore moving it into a separate class.

An early indicator of this can be when related methods are physically grouped together in a class. Especially when those methods don't have anything to do with other parts of a class. Our guitar had a lot of different methods for each of the different tunings. We moved those out of the class and into another that's responsible for all possible tunings.

Even if this class isn't reused or composed in other classes, there's value here. We've freed up complexity inside the Guitar class, while not taking away any of its ability. We can still tune the guitar to any number of tunings. We don't need to worry ourselves with the implementation details of how most of the time. And when we do need to dig into how a guitar gets tuned, we know where to look. We don't need to dig into the depths of various private methods in Guitar. We can start with our aptly-named Tuner class.

Play Hard

We make sounds on our guitar by plucking the strings with one hand. The other hand presses down on the strings on the neck of the guitar. The neck has many sections called frets. Pressing down on each of these plays a higher frequency as you move up the neck towards your other hand.

Our GuitarString knows which note we play, based on what note it's tuned to and which fret our hand is on. But we don't say we play the guitar strings - we play the guitar.

class Guitar
  def pick(string:, fret:)
    @strings[string -1].pluck(fret: fret)
  end
end
Enter fullscreen mode Exit fullscreen mode

With this implementation, we get back the note of the sound the guitar makes.

guitar = Guitar.new
guitar.tune
guitar.pick(string: 6, fret: 1)
=> :f
Enter fullscreen mode Exit fullscreen mode

Compose Collaborators

Much like with our tuning, our public interface is through the guitar. Again, the majority of our work isn't done by the Guitar class. Its responsibility is taking the input and passing it off to a collaborating class. Here, it figures out which of the strings is being played, and sends it the pluck method.

The GuitarString handles the hard work of which musical note comes out of the guitar. The Guitar knows how to work with its strings to achieve the result that the caller asked for.

Floor It

It's not enough to know which note we're playing. It's a start, but doesn't tell the full story about how what we play sounds. We also need to know which octave of the note we're playing.

We'll change our GuitarString class to return not only the note, but also the octave. We return both elements in an array.

class GuitarString
  def pluck(fret:)
    [note, octave]
  end
end
Enter fullscreen mode Exit fullscreen mode

Now let's display all this data.

note = guitar.pick(hand_position)
"#{note.first}#{note.last}"
Enter fullscreen mode Exit fullscreen mode

We know that the first element is the note, and the last element is the octave. We know that because we just wrote it, and it's sitting right above our use of it. However, it's not obvious in other cases what each of these elements refers to.

We'll address that by building a custom Note class and returning it in GuitarString#pluck.

class GuitarString
  def pluck(fret:)
    Note.new(
      starting_note: @tuning_note,
      starting_octave: @tuning_octave,
      offset: fret,
    )
  end
end
Enter fullscreen mode Exit fullscreen mode

That will change our display logic.

note = guitar.pick(hand_position)
"#{note.value}#{note.octave}"
Enter fullscreen mode Exit fullscreen mode

Now it's clear what the data is that we're displaying. It's not the first element, it's the note value. It's not the last element, it's the note octave.

We could achieve a similar result by using a Hash. That will allow us to name the data elements we refer to in our display logic.

By moving to a separate class, we can also associate behavior with this data.

class Note
  def value; end
  def octave; end

  def to_s
    "#{value[0].upcase}#{'b' if flat?}#{octave}"
  end
end
Enter fullscreen mode Exit fullscreen mode

Now our display logic doesn't even need to know about the internals of the Note class. Instead, it just needs to ask it to display itself.

guitar.pick(hand_position).to_s
Enter fullscreen mode Exit fullscreen mode

Elevate Primitives to Objects

We needed to return a collection of information out of our GuitarString#pluck method. Callers need both the note and octave to know what our output sounds like. We started with a primitive data structure from Ruby, an Array.

That worked, but wasn't very clear what all the data elements represented. We can bring clarity to that with another primitive, a Hash. Later on, we wanted to exercise custom behavior on top of this collection of data. To do that, we made a separate class that encapsulates this data and related behavior.

That's Technical Talk

We now have a system that knows how to play guitar just like Stevie Ray Vaughan did. Except for all the talent, the feeling, the creativity, and the humanity that went into his playing.

Along the way, we reinforced concepts by using domain terminology. We identified related behavior within a class and extracted it to a separate class. We collaborated with those extractions to build up our system. Our public interfaces (like the Guitar#pick method) don't need to house the complexity. And we built more classes to replace primitive data structures. When we identified behavior related to that data, we had a natural landing place for it.

I hope this Texas-sized flood of information helps in your next domain modeling exercise.

Top comments (1)

Collapse
 
franiglesias profile image
Fran Iglesias

Superb explanation. I loved the example.