saebekassebil / teoria

Javascript taught Music Theory
http://saebekassebil.github.io/teoria
MIT License
1.32k stars 114 forks source link

New Feature: TeoriaProgression #65

Closed JasonStorey closed 8 years ago

JasonStorey commented 9 years ago

TeoriaProgression

A new feature for constructing and managing diatonic chord progressions.

I needed this for a project, so I added it!

First PR, so apologies if this is no good. Let me know if it's not worthy... or if you don't think this belongs in Teoria.

var cMajScale = teoria.scale('c', 'major');
var twoFiveOne = teoria.progression(cMajScale, [2, 5, 1]);

twoFiveOne.getChord(0).toString(); // 'Dmin'
twoFiveOne.getChord(1).toString(); // 'GM'
twoFiveOne.getChord(2).toString(); // 'CM'

twoFiveOne.simple(); // [['d3','f3','a3'], ['g3','b3','d4'], ['c3','e3','g3']]

I've only implemented TeoriaProgression.simple() & TeoriaProgression.getChord(n) so far.

Because of the way the chords are built, this will only work for the standard modes. God knows what kind of exotic harmony would be generated from the pentatonics or blues scales. This needs sorting... I wanted to get your opinion before I continue though.

It'd be cool to add...

Feedback welcome!

saebekassebil commented 9 years ago

I've been thinking about a feature exactly like this! Let me just ponder it for a little while, and I'll get back to you - cheers Jason!

JasonStorey commented 9 years ago

Ace! Ponder away.

Feel free to tear it apart... I was learning your library as I went so there are certainly better ways to do this.

saebekassebil commented 9 years ago

So Jason. First of all, this is a very important feature! Chord progressions are one part of the "horizontal axis". We/I'll need to figure out ways of representing and working with note sequences as well.

Anyway, what do you think about making this a npm module? E.g.:

var teoria = require('teoria');
var progression = require('teoria-chord-progression'); // or whatever name
var toArabic = require('roman-numerals').toArabic;

var scale = teoria.scale('c', 'major');
var jazzyCadence = progression(scale, [2, 5, 1]);
var authenticCadence = progression(scale, ['V', 'I'].map(toArabic));
JasonStorey commented 9 years ago

Representing note sequences would be great! I guess a 'melody' object is what we want. I think that belongs in a separate module.

Similarly, I suppose, the progression object should be in a separate module. Objects like melody and progression are higher level than note, interval, scale, and chord.

Okay, so pulling this out into a separate module is probably wise, but I don't see how it works in your example... Progression depends on Teoria (it uses chord, interval, etc.) so you'd have to pass it a reference to teoria to use internally.

Something like...

var teoria = require('teoria');

var progression = require('teoria-chord-progression')(teoria); 

//Progression is passed a reference to Teoria,
//and can extend it by adding the teoria.progression method to the API

var scale = teoria.scale('c', 'major');
var jazzyCadence = teoria.progression(scale, [2, 5, 1]);
saebekassebil commented 9 years ago

So there's a fundemental problem (for now) with teoria - it's not modularized properly. Which means that you can't just require the few modules you need. However in this particular case you shouldn't have to pass a reference to teoria, as you can just use the TeoriaNote objects directly when passed as arguments.

// module: teoria-chord-progression
var piu = require('piu'); // A module I've made for inferring chord names from a set of notes
module.exports = function(scale, progress) {
  var notes = scale.notes(), length = notes.length;
  chordlength = chordlength || 3;

  var chords = progression.map(function(cindex) {
    for(var i = 0, chordnotes = []; i < chordlength; i++)
     chordnotes.push(notes[(cindex - 1 + 2 * i) % length]);

    return piu.name(piu.infer(chordnotes)[0]);
  });

   return chords;
}

see how there's no reference to teoria needed?

JasonStorey commented 9 years ago

Sure, using the notes directly isn't a problem, but shouldn't progression contain an array of TeoriaChords?

There is no reference to TeoriaChord in the scale object. Were you imagining it would work differently?

saebekassebil commented 9 years ago

You can use the TeoriaNote#chord method to create the chords, since you've already got an array of TeoriaNotes from scale.notes()?

I know this is a bit of a problem, and I'm working to find a solution to make teoria more modular (while still maintaining a "sugar" version with syntax like the current version)

JasonStorey commented 9 years ago

Ah yes... I understand now. I might move this functionality into a separate 'teoria-chord-progression' repo then. Or would you rather do it?

saebekassebil commented 9 years ago

No please, go ahead Jason. If you could message me somehow when you've done it, I'd be grateful.

JasonStorey commented 9 years ago

I had a chance to start on the weekend... https://github.com/JasonStorey/teoria-chord-progression

Not yet published to NPM. Super limited functionality at the moment. Let me know what you think so far.

There is one sad part I've found; perhaps you have a better solution...

To construct chords using the TeoriaNote.chord() method, I need to pass the chord name string without the note name prefix -


var chordNotes = [/* TeoriaNotes for F#maj7 */]

var chordName = piu.name(piu.infer(chordNotes)[0]); // 'F#maj7'

// First, need to remove 'F' and '#' from chordName
chordName = chordName.slice(1).replace(/^(b+|#+)/, '');

// then, pass 'maj7' to TeoriaNote.chord()
var chord = degreeInScale.chord(chordName);

Ideally, I'd just pass the inferred chord name straight to teoria.chord and everything is done.

dstaubsauger commented 9 years ago

I haven't tested it against the existing tests, but why not have the .slice(1) be part of the regex like

chordName = chordName.replace(/^([A-G]|b|#)+/, '');
JasonStorey commented 9 years ago

Sure, that's prettier. Done!

I was really hoping there might be a way to construct a TeoriaChord from the inferred chord name, without needing the regex. I can't do it from TeoriaNote.chord. I guess it's not important though... this works.

Any other pointers before I publish? I'll also add...

saebekassebil commented 9 years ago

Hej @JasonStorey - beautiful work! We'll need a feature to just get the chord without the note name!

I personally prefer .interval instead of .transpose (returning a new object instead of mutating the existing one) - but that's just me.

saebekassebil commented 8 years ago

I believe this is resolved with your teoria-chord-progression module?