-
Notifications
You must be signed in to change notification settings - Fork 64
Core music theory library
This document won't cover all methods, classes and details. This is more like a tour to give you an idea of what the library is capable of. To have a deeper view, I invite you to read the specs in the /specs
folder of the library and to play with the lib yourself via bin/console
.
- Setup
- Tuning
- Frequencies
- Pitches
- Voicings
- Pitch-Classes
- Notes
- NoteSets
- Frequency Intervals
- Intervals-Classes
- Intervals
- Chord Qualities
- Chords
- Scales
- Roman Chords
- Progressions
Add gem 'coltrane'
to your Gemfile
or do your thing to have this gem available. I'm sure you will know how to do it. Then you'll be able to:
require 'coltrane'
Now you have the core music theory available (Coltrane::Theory
). The Command Line Interface, Synth, Instruments (representation), Commands, anything else, won't be imported by default. This is by choice, to keep your application light.
To make it more accessible on your class you do a include Coltrane::Theory
. That will allow you to access Scale.major('C')
instead of Coltrane::Theory::Scale.major('C')
.
The default tuning of the whole system is 440hz for A4. If you need to change, just do Coltrane.tuning = 430
, for example.
The whole system is organized around frequencies. They can be both created by []
or .new()
methods. Frequency operations involve some logarithmic calculation so this class provides you handy methods:
Frequency[440].octave(1)
#=> 880hz
Frequency[440].octave_up
#=> 880hz
Frequency[880].octave_down
#=> 440hz
You can sum, subtract and compare frequencies:
Frequency.new(200) + Frequency.new(120.3333)
#=> #<Coltrane::Frequency:0x00007fb3d42c0878 @frequency=320.3333>
Frequency.new(200) - Frequency.new(120.3333)
#=> #<Coltrane::Frequency:0x00007fb3d5962fb8 @frequency=79.6667>
Frequency[300] == Frequency[300]
#=> true
Frequency[400] > Frequency[300]
#=> true
Divisions between frequencies return the interval between both in cents
. 100 cents are equivalent to 1 semitone and 2 to one tone:
Frequency[880] / Frequency[440]
#=> #<Coltrane::Interval:0x00007fb3d4291b90 @cents=1200>
While Frequency is a random frequency that may have any kind of sound, pitches are sounds that are "in tune". They have frequency information that will always represent a note and an octave, such as E4
or A7
.
Via #new
:
Pitch.new('C3')
Pitch.new(note: 'C', octave: 3)
Pitch.new(frequency: 15.434)
# Via MIDI number
Pitch.new(12).name
Or via []
:
Pitch['C3']
# Via MIDI number
Pitch[39]
Obtaining Scientific Pitch Notation from MIDI number:
Pitch[12].name
#=> "C0"
Frequency information:
Pitch['A4'].frequency
#=> #<Coltrane::Frequency:0x00007fb3d3975548 @frequency=440.0>
Math operations:
(Pitch['C3'] + 12).name
#=> 'C4'
(Pitch['C3'] - 12).name
#=> 'C2'
Voicings are groups of pitches. Don't confuse with Chords. They are more like an actual concrete implementation of music, since pitches have octave information while Note or PitchClasses don't. They were built mainly to aid synthesizer libraries.
In the future, this class will actually be able to aid on creating pleasant chord voicings, by understanding the minimum distance frequencies should be apart, taking into account the generally known thumb rule that lower pitches need more space to sound good than higher ones.
Voicing['C3', 'A8', 'G4']
The only really useful method for now is #frequencies
Voicing['C3', 'A8', 'G4'].frequencies
=> [
#<Coltrane::Frequency:0x00007fcf2c8f5478 @frequency=130.8127826502993>,
#<Coltrane::Frequency:0x00007fcf2c8f5338 @frequency=7040.0>,
#<Coltrane::Frequency:0x00007fcf2c8f51f8 @frequency=391.99543598174927>
]
Pitch-classes are all the classes of pitches that are in a whole number of octaves apart.
BEWARE class
part of the word here is not in the sense of Ruby or computer science at all. This is a technical name created by the music experts, totally outside of the software development world.
For example, C1, C2, C3, C4 are pitches from the C pitch class.
Why to not just call it Note instead? Because notes are technically something really different from most people think. You learn why on the next topic.
You can get one by frequency:
PitchClass.new(frequency: 261.63).notation
#=> "C"
Or by the note name:
PitchClass.new('A')
PitchClass['A']
You can obtain the interval by subtraction:
(PitchClass['G'] - PitchClass['A']).full_names
#=> ["Minor Seventh", "Augmented Sixth", "Minor Fourteenth", "Augmented Thirteenth"]
Sum
(PitchClass['C'] + 2).name
=> "D"
# Against frequencies, the result will round up to closest PitchClass
(PitchClass['C'] + Frequency[220]).name
=> "A#"
As PitchClasses will comprise a whole array of frequencies, they have what we call a fundamental frequency
, which is the Frequency at 0 octave:
PitchClass['A'].fundamental_frequency.octave_up(4)
#=> #<Coltrane::Frequency:0x00007ff57ca6c768 @frequency=440.0>
A Note is a very abstract concept. You can think Notes as being the different ways of representing PitchClasses. Take D#
and Eb
for example, they're different Notes with the same PitchClass (D#), in a equal-tempered scale (if you don't know what that means, don't worry. If you ever used something different than that in your life, you'd probably know).
I.e, notes have sharps (#) and flats (b). In Coltrane
, they can have as much as they want. Inside the system, because of lacking a better name, we call them alterations. So for example, C#
has an alteration of +1. Db
has an alteration of -1. The idea of alteration is altering its name but preserving the PitchClass (like C#
vs Db
)
Note['C#'].alter(-1).name
#=> "Db"
You can try to change the note's alteration, but that may not always succeed, because of the pure logic of it. For example: C#
can never have its alteration to 0. That means you can never express it without using a sharp or a flat.
# Following example is impossible
Note['C#'].alter(0).name
#=> "C#"
# But this one is perfectly possible
Note['B#'].alter(0).name
#=> "C"
Notes class inherit from PitchClass, so everything from PitchClass mostly applies to Notes.
Note['C#']
Note['C##']
Note['Cbb']
Because we most of the time want C#
to be treated equally as Db
, this is baked in the system:
Note['B#'] == Note['C']
#=> true
If for some reason you really want to stress the difference, compare them by name:
Note['B#'].name == Note['C'].name
#=> false
#alter
:
Note['D#'].alter(-1).name
#=> "Eb"
#as
Note['B'].as('C').name
#=> "Cb"
Note['A'].as('F').name
#=> "F####"
Note inherit from PitchClass, so you can expect that all methods from the last
work just fine, such as: :+
, :-
, #frequency
, etc.
#pretty_name
Note['C#'].pretty_name
=> "C♯"
And a lot of accident asking methods, such as: #accident?
, #sharp?
, #flat?
, #double_sharp?
, #double_flat?
NoteSet is simply a group of notes. They provide a confortable way of working
with these. Basically every method in Coltrane
that outputs a set of notes
should output a NoteSet
(if you find something that doesn't, please open an
issue).
NoteSet[*%w[C# G# A B Bb]]
If you're not familiar with that syntax, this is essentially the same as:
NoteSet['C#', 'G#', 'A', 'Bb']
A very important thing: NoteSet
cannot contain more than 1 note for each
PitchClass
. That means the following code:
NoteSet['C#', 'Db']
Will actually return the same as:
NoteSet['C#']
That way, we guarantee, on Chord Building for example, that things run smooth. Repeated pitch classes have no advantages for the library and just add complexity.
#names
basically return an array of strings
NoteSet['C#', 'G#', 'A', 'Bb'].names
=> ["C#", "G#", "A", "Bb"]
#pretty_names
NoteSet['C#', 'G#', 'A', 'Bb'].names
#=> ["C♯", "G♯", "A", "B♭"]
You can intersect NoteSets:
intersection = NoteSet['C#', 'D', 'G', 'G#'] & NoteSet['Db', 'F', 'A', 'G#']
intersection.names
#=> ["C#", "G#"]
Notice it intersected 'C#' against 'Db', since both are enharmonic (same PitchClass).
That feature allows you cross notes from chords or scales:
common_notes = Scale.major('C').notes & Chord.new(name: 'C#6/9').notes
common_notes.names
#=> ["F"]
Frequency Intervals are basically the distance between 2 frequencies. They're measured in a logarithmic way (cents). They may be #ascending?
or #descending
.
A subclass of Frequency Intervals. In the same sense that Pitch Classes are a class(group, category, not Ruby Class) of all pitches across octaves, Interval-Classes are categories of intervals. In the system, they're pretty much what we'd call Major Third, Perfect Fifth and so on. They are pretty much what a Minor Third and Augmented Second have in common: both share the same logarithmic distance.
Intervals are a subclass of Interval Classes. They carry more details and are more suitable for Music Theory use.
Many many ways of retrieving them. Bear with me:
interval = Note['D'] - Note['C']
#=> #<Coltrane::IntervalClass:0x00007ff57a373d28 @cents=200>
interval.name
#=> "M2"
Interval.major_second
#=> #<Coltrane::IntervalClass:0x00007ff57a9c59b0 @cents=200>
They may be compound and altered (a name I made up for aug/dim intervals):
Interval.augmented_thirteenth
Coltrane::Interval.augmented_thirteenth
=> #<Coltrane::Interval:0x00007f958e3f79d0 @cents=1000, @compound=true, @letter_distance=6>
Interval.major_second
You sum them against notes:
(Note['C'] + Interval.major_second).name
#=> "D"
You can invert them:
(-Interval.minor_third).full_name
#=> Major Sixth
Interval-Sequences are a way of expressing a sequence of intervals (duh). They're needed mostly for Chords and Scales.
You can build using notes. Notice it will only retrieve the interval information from the notes and discard them completely after.
IntervalSequence.new(notes: %w[D F C A])
IntervalSequence.new(intervals: [0, 3, 5, 7, 10])
This is a very important class, since it is the core functionality of many important aspects of the library such as scale building/finding, chord building/finding. Thereby, it's packed with a lot of functionalities to aid on these tasks.
#distances
If you're familiar with the Major Scale, you know by heart the following sequence: W W H W W W H
, W meaning Whole Tone and H meaning half tone. In Coltrane
, we call these distances.
That's how the above example works:
IntervalSequence.new(intervals: [0, 2, 4, 5, 7, 9, 11]).distances
[2, 2, 1, 2, 2, 2, 1]
And btw, you can also instantiate a interval sequence via distances:
IntervalSequence.new(distances: [2, 2, 1, 2, 2, 2, 1]).distances
But in that case, you'd probably prefer to just use Scale.major
😘
Suppose you want to know what kind of third an interval sequence has:
interval_sequence.third
#=> "Major Third"
Let's say you wanna know the sixth:
interval_sequence.sixth
#=> "Augmented Sixth"
What if you only want it to return it if its major, minor or perfect, not considering augmented or diminished
interval_sequence.sixth!
#=> nil
You can also ask in a boolean way:
interval_sequence.has_major_third?
interval_sequence.has('Perfect Fifth')
And several other methods that we will explore on its children: ChordQuality
and Scale
A ChordQuality is a special case of IntervalSequence because they have a special sequence of intervals, which makes possible to name it. The system has actually a tree of chords and uses that to define its name. ChordQualities are mostly used by the Chord class.
That's where things start to get interesting. So, Chord essentially has 2 attributes: a Note (root note) and a ChordQuality.
It can be built in many different ways:
Chord.new(notes: %w[C E G]).name
#=> CM
Chord.new(name: 'A7').notes.names
#=> ["A", "C#", "E", "G"]
Chord.new(root_note: Note['C'],
quality: ChordQuality.new(name: 'm7')).notes.names
#=> ["C", "D#", "G", "A#"]
Chords can return their notes (as NoteSet, always), as you saw below.
Chords also can be transposed:
(Chord.new(name: 'C6/9') + Interval.major_second).name
#=> "D6/9"
You can add notes to it:
(Chord.new(name: 'DM') + Note['B']).name
#=> "DM6"
This is like an experimental feature. Chord substitutions are a very common technique since Jazz Bebop area. For now, the only substitution available is #tritone_substitution
Chord.new(name: 'Cm7').tritone_substitution.name
#=> "F#m7"
Scales are a cyclic sequence of notes. They're actually composed by an IntervalSequence and a starting note (tone). They may also have a name.
You may provide distances and a tone:
Scale.new(2, 2, 1, 2, 2, 2, 1, tone: 'D')
You may simply provide notes:
Scale.new(notes: %w[C D E F G A B])
Since there many known scales and modes, a module called Classic Scales extends Scale with some scale building and finding functionality. This module has some classic scales built-in:
SCALES = {
'Pentatonic Major' => [2, 2, 3, 2, 3],
'Blues Major' => [2, 1, 1, 3, 2, 3],
'Harmonic Minor' => [2, 1, 2, 2, 1, 3, 1],
'Hungarian Minor' => [2, 1, 2, 1, 1, 3, 1],
'Pentatonic Minor' => [3, 2, 2, 3, 2],
'Blues Minor' => [3, 2, 1, 1, 3, 2],
'Whole Tone' => [2, 2, 2, 2, 2, 2],
'Flamenco' => [1, 3, 1, 2, 1, 2, 2],
'Chromatic' => [1] * 12
}.freeze
Those basically the distances and they will allow you to build scales by doing:
Scale.pentatonic_major('C')
Scale.blues_minor('D')
Since Diatonic Scale is a very special scale on the Western Music and that it needs notes to be outputted in a certain way (alteration), they are treated as a special case of Scale
. To create it:
Scale.major('C')
Scale.minor('F')
They have a special method such as #relative
that will return the relative major/minor.
#having_notes
Scale.having_notes NoteSet['C', 'E', 'G']
#=> huge list of scales indexed by tone and scale name
Scale.having_chords('CM7', 'Dm7')
#=> huge list of scales indexed by tone and scale name
Since now you know all the ways you can get yourself a scale, here's what you can do with them:
#triads
, #sevenths
, tertians(size)
These will return the chords of the scale following the formula of choosing a degree and skipping the next.
#chords(size)
Return all known chords of that scale having the given size
#sharps
, #flats
, #accidentals
, you understand what they do.
#degree
, #[]
, that will return you a note of a certain degree of the scale
and of course, #notes
will give you the notes.
Roman Chords are basically a way of describing chords relative to their key using roman numeral notation. In the key of C, for example, C7 chord may be describe as I7, Dm as ii, G7 as V7, etc. The reason they were added to the library was to unleash the power of Progressions. Check more details on this wikipedia article.
RomanChord.new('I', key: 'C').chord.name
#=> "CM"
RomanChord.new(chord: 'CM', key: 'C').notation
#=> "I"
As they are mostly used within Progressions, lets skip this section
Progressions are way of visualizing harmony in music by using Roman Chords. That way, we can recognize the same patterns across many different songs, such as the most over-used progression in the world I-V-vi-IV
, that can be found anywhere, from Lady Gaga's Pokerface to Red Hot Chilli Pepper's Otherside (video explaining).
The most straightforward way to create progressions is by using Progression Notation. Basically, Roman Chords
progression = Progression.new('I-IV-vi-V', key: 'A')
progression.chords.map(&:name)
#=> ["AM", "DM", "F#m", "EM"]
Progression.new('IM7-IV7-vi-Vdim7', key: 'B').chords.map(&:name)
#=> ["BM7", "E7", "G#m", "F#dim7"]
You can find progressions by providing some chords. It will scan keys (scales) and return how that sequence of chords could be described on that scale, sorted by notes that are outside of the scale.
Progression.find('AM', 'DM', 'F#m', 'EM)
#=> big list of progressions sorted by notes out of key
Other methods are: #notes
, #chords
, #notes_out
This a module that extends Progression. It allows to create well known progressions very easily:
Progression.jazz('D').chords.map(&:name)
=> ["Em7", "A7", "D7"]
Progression.pop('F#').chords.map(&:name)
=> ["F#M", "C#M", "D#m", "BM"]
Progression.blues('G#').chords.map(&:name)
=> ["G#M7", "C#7", "G#7", "D#7", "C#7", "G#7"]