Skip to content

Instantly share code, notes, and snippets.

@lgvr123
Created February 22, 2022 23:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lgvr123/ad3aa4ffeda8effbec6d8b6fc329e288 to your computer and use it in GitHub Desktop.
Save lgvr123/ad3aa4ffeda8effbec6d8b6fc329e288 to your computer and use it in GitHub Desktop.
ChordAnalyzer {
classvar <pitches;
classvar tests;
classvar <roots;
classvar <defaults;
*initClass {
pitches=[ [ 'Fbb' , 3 ] , [ 'Cbb' , 10 ] , [ 'Gbb' , 5 ] , [ 'Dbb' , 0 ] , [ 'Abb' , 7 ] , [ 'Ebb' , 2 ] , [ 'Bbb' , 9 ] , [ 'Fb' , 4 ] , [ 'Cb' , 11 ] , [ 'Gb' , 6 ] , [ 'Db' , 1 ] , [ 'Ab' , 8 ] , [ 'Eb' , 3 ] , [ 'Bb' , 10 ] , [ 'F' , 5 ] , [ 'C' , 0 ] , [ 'G' , 7 ] , [ 'D' , 2 ] , [ 'A' , 9 ] , [ 'E' , 4 ] , [ 'B' , 11 ] , [ 'F#' , 6 ] , [ 'C#' , 1 ] , [ 'G#' , 8 ] , [ 'D#' , 3 ] , [ 'A#' , 10 ] , [ 'E#' , 5 ] , [ 'B#' , 0 ] , [ 'F##' , 7 ] , [ 'C##' , 2 ] , [ 'G##' , 9 ] , [ 'D##' , 4 ] , [ 'A##' , 11 ] , [ 'E##' , 6 ] , [ 'B##' , 1 ] ];
tests=[
(condition: ["M7","maj7","t7"], vals: (n3: 4, n5: 7, n7: 11, def6: 9 ), nature:"Major7"),
(condition: ["Maj","Ma","M","maj","ma"], vals: (n3: 4,n5: 7,def6: 9, def7: 11), nature:"Major"),
(condition: ["min","mi","m","-"], vals: (n3: 3,n5: 7,def6: 8, def7: 10), nature:"Minor"),
(condition: ["dim","o", "°"], vals: (def2:1, def4:4, n3: 3, n5: 6, n7: 9, def6: 7 ), nature:"Diminished"),
(condition: ["0"], vals: (n3: 3, n5: 6, n7: 10, def6: 8 ), nature:"Half-Diminished"),
(condition: ["aug","+"], vals: (n3: 3, n5: 6, def7: 9, def6: 8 ), nature:"Augmented"),
(condition: ["sus2"], vals: (n3: 2, n5: 7, def6: 9 ), nature:"Sus2"),
(condition: ["sus4"], vals: (n3: 4, n5: 7, def6: 9 ), nature:"Sus4"),
];
roots = IdentityDictionary[
$# -> ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"],
$b -> ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"],
$M -> ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"],
$m -> ["C", "Db", "D", "D#", "E", "F", "F#", "G", "G#", "A", "Bb", "B"],
];
defaults = IdentityDictionary[
$M -> [$b, $b, $#, $b, $#, $b, $#, $#, $b, $#, $b, $b],
$m -> [$b, $#, $b, $#, $#, $b, $#, $b, $#, $#, $b, $#],
];
}
*analyze {|chordName|
var analyzer;
var res;
var rootTxt, bassTxt, gt, p, bgt, bassnote;
if (chordName.isNil) {Exception("chordName cannot be nil").throw;};
if (chordName.isKindOf(String).not) {Exception("chordName must be a String").throw;};
// la basse de l'accord
#rootTxt, bassTxt=chordName.split($/);
analyzer=ChordAnalyzerHelper.new(ChordAnalyzerHelper.substr(rootTxt,1));
rootTxt=rootTxt[0];
// Le root de l'accord
["bb","b","x","#"].do({|sign| if(analyzer.beginsWith(sign)){rootTxt=rootTxt++sign;}});
p=pitches.collect(_[0]).indexOf(rootTxt.asSymbol);
if (p.isNil) {Exception.new(format("Invalid chord root name: %",bassTxt)).throw};
gt=pitches[p][1];
if (bassTxt.notNil){
p=pitches.collect(_[0]).indexOf(bassTxt.asSymbol);
if (p.isNil) {Exception.new(format("Invalid chord bass name: %",bassTxt)).throw};
bgt=pitches[p][1];
bassnote=bgt-gt-12;
if(bassnote<=(-12)) {bassnote=bassnote+12};
};
// Le nommage de l'accord
tests.do({ |c, i|
if(res.isNil) {
var x;
x=analyzer.beginsWith(c.condition);
if(x) {
res=c.vals;
res.nature=c.nature;
};
};
});
res=res?(n3: 4,n5: 7,def6: 9, def7: 11, nature: "Major");
// Compléments
res.note=Set.new.add(0);
res.semis=Set.new.add(0);
// ..7..
if (res.n7.notNil) {
res.note.add(res.n7);
}{
if (res.n7.isNil.and(analyzer.contains("7"))) {
res.n7 = 10;
res.note.add(res.n7);
} { if(res.n7.isNil.and(res.def7.notNil)) {
res.n7 = res.def7;
} {
res.n7=11;
}
}};
if(res.n7.notNil) {res.semis.add(res.n7)};
// ..5..
if (analyzer.contains("b5")) {
res.n5 = 6;
}{
if (analyzer.contains("#5")) {
res.n5 = 8;
}
};
if(res.n5.notNil) {
res.semis.add(res.n5);
res.note.add(res.n5);
};
// ..2/9..
// Always out a 2nd in the scale
if (res.def2.notNil) {
res.semis.add(res.def2);
}{
res.semis.add(2);
};
// And a the 9th if specified
if (analyzer.contains("b9")) {
res.semis.add(1);
res.note.add(13);
}{
if (analyzer.contains("#9")) {
res.semis.add(3);
res.note.add(15);
}{
if (analyzer.contains("9")) {
res.semis.add(2);
res.note.add(14);
}
}
};
// ..4/11..
if (analyzer.contains("b11")) {
res.semis.add(4);
res.note.add(16);
}{
if (analyzer.contains("#11")) {
res.semis.add(6);
res.note.add(18);
}{
if (analyzer.contains("11")) {
res.semis.add(5);
res.note.add(17);
}{
if (res.def4.notNil) {
res.semis.add(res.def4);
}{
res.semis.add(5);
}
}
}
};
// ..13..
if (analyzer.contains("b13")) {
res.semis.add(8);
res.note.add(20);
}{
if (analyzer.contains("#13")) {
res.semis.add(10);
res.note.add(22);
}{
if (analyzer.contains("13")) {
res.semis.add(9);
res.note.add(21);
}{
if (res.def6.notNil) {
res.semis.add(res.def6);
}{
res.semis.add(9);
}
}
}
};
if(res.n3.notNil) {
res.note=res.note++res.n3;
res.semis=res.semis++res.n3;
};
if(bassnote.notNil) {res.note.add(bassnote)};
^(name: chordName, note: res.note.asArray.sort, scale: res.semis.asArray.sort, gtranspose: gt, nature: res.nature, rootname: rootTxt);
}
*rootChord { arg event, sign, fullname=false;
var out, minor=true, notes, copy;
// Rem: working on a copy of the event for avoiding any modification of it
copy=event.clone;
copy.parent_(Event.partialEvents.pitchEvent);
copy.note=copy.use{copy.note}; //on force l'utilisation de la méthode
// event.use{ // <-- STOP car semble faire perdre les pinceaux à SC pour ce qui est dans `currentEnvironment
// var out, minor=true, notes;
if(copy.name.notNil.and((copy.usename?true).or((fullname?false).asBoolean)))
{
// A name is specified in the progression
out=copy.name
}
{
minor= copy.isMinor;
// if(copy.notes.isNil.or(notes.isKindOf(Array).not)) { notes=[0,4,7]} { notes=notes.collect(_.asInteger)};
// notes=copy.note.collect(_.asInteger);
notes=copy.note;
if(sign.isNil) {
// scale.postln;
if (minor) {sign=$m;} { sign=$M}
};
if(sign.class == Symbol) {sign = sign.asString};
if(sign.class == String) {sign = sign[0]};
out = roots.at(sign)[copy.gtranspose.round(1.0) % 12];
out=out++(if(minor){"-"}{""});
if(notes[0]!=0) { out = out ++"/"++ roots.at(sign)[(copy.gtranspose+notes[0]).round(1.0) % 12] };
// };
}
^out;
}
}
ChordAnalyzerHelper {
var <initial;
var <remaining;
*new { | initialText |
^super.new.init(initialText)
}
init { | initialText |
if (initialText.isNil) {Exception("initialText cannot be nil").throw;};
if (initialText.isKindOf(String).not) {Exception("initialText must be a String").throw;};
initial=initialText;
remaining=initialText;
}
reset {
this.init(initial);
}
*substr {|str, from=0, to=nil|
var res="";
if((str?"").size>0)
{
to=to?99999;
res=str.copyRange(from,to);
}{
"Cannot extract from empty string".error;
};
^res?"";
}
beginsWith {|test|
var res;
if(test.isNil) {Exception("Cannot test again a nil string").throw;};
// (test?"---").debug("In BW (before analyze)");
if(test.isNil){test=[]};
if(test.isKindOf(Array).not){test=Array.with(test)};
// (test?"---").debug("In BW (after analyze)");
if(remaining.isNil) {remaining="XXXXXXXX"};
// remaining.debug("remaining");
res=test.collect({
|t, i|
// remaining.debug(format("remaining at %",i));
// t.debug("searching");
if (remaining.beginsWith(t)){
// "found".postln;
remaining=ChordAnalyzerHelper.substr(remaining,t.size);
// remaining.debug("after");
true;
}{
// "not found".postln;
false;
}
}
).reduce({|a,b| a.or(b)});
^res?false;
}
contains {|test|
var res;
// (test?"---").debug("In CNT (before analyze)");
if(test.isNil){test=[]};
if(test.isKindOf(Array).not){test=Array.with(test)};
// (test?"---").debug("In CNT (after analyze)");
if(remaining.isNil) {remaining=""};
// remaining.debug("remaining");
res=test.collect({
|t, i|
var pos;
// remaining.debug(format("remaining at %",i));
// t.debug("searching");
pos=remaining.find(t);
if (pos.notNil){
// pos.debug("found at");
remaining=ChordAnalyzerHelper.substr(remaining,0,pos-1)++ChordAnalyzerHelper.substr(remaining,pos+t.size);
// remaining.debug("after");
true;
}{
// "not found".postln;
false;
}
}
).reduce({|a,b| a.or(b)});
^res?false;
}
printOn { |stream|
(initial: initial.quote, remaining: remaining.quote).printOn(stream);
// [initial, remaining].printOn(stream);
}
}
+String {
asChordEvent {
^ChordAnalyzer.analyze(this);
}
playChordNotes { |verbose=false|
this.asChordEvent.playNotes(verbose: verbose);
}
playScaleNotes {|verbose=false|
this.asChordEvent.playScaleNotes(verbose: verbose);
}
}
+Event {
clone {
^()++this;
}
asChordName {
^ChordAnalyzer.rootChord(this, fullname:true);
}
isMinor {
var notes;
notes=if(this.note.notNil) {
// we have notes
this.note.asArray;
}{
// If we don't have notes, we are looking for a scale.
if(this.scale.isKindOf(Scale)){this.scale.semitones}{this.scale};
};
// if we still don't have notes, we say that we are in major
if (notes.isNil) { ^false};
^notes.indexOf(3).notNil.and(notes.indexOf(4).isNil);
}
adaptScaleFrom {
|source|
var srcSemis,chordnotes,semitones, naturals;
srcSemis=if(source.scale.respondsTo(\semitones)) { source.scale.semitones}{source.scale};
srcSemis=(srcSemis+source.gtranspose).mod(12).sort;
srcSemis=(srcSemis.addAll(srcSemis+12));
naturals=if(this.scale.respondsTo(\semitones)) { this.scale.semitones}{this.scale};
naturals=(naturals+this.gtranspose).mod(12).sort;
naturals=(naturals.addAll(srcSemis+12));
chordnotes=if (this.respondsTo(\note).not) {
// si on n'a pas de note dans k'event, alors on prend une copie et on force le calcul des notes
var tmp=this.clone;
tmp.parent_(Event.partialEvents.pitchEvent);
tmp.note=tmp.use{tmp.note}; //on force l'utilisation de la méthode
tmp.note;
}
{
this.note
};
chordnotes=(chordnotes+this.gtranspose).mod(12).sort;
semitones=chordnotes.collect{|note, index|
var down=note;
var up=chordnotes.at(index+1)?(chordnotes[0]+12);
var expand=[down];
while({(up>down).and((up-down)>2)}){
var desired, missing;
/*[down, up].postln("from/to");*/
// looking for the next natural semitones
desired=naturals.at(naturals.indexOfGreaterThan(down))/*.debug("natural")*/;
// if this natural semitones is part of the previous chord we keep it, otherwise we take it from the previous chord
missing=if (srcSemis.indexOfEqual(desired).notNil) {
desired/*.debug("found")*/
}{
srcSemis.at(srcSemis.indexOfGreaterThan(down))/*.debug("selected")*/
};
if (missing<up) {
expand.add(missing);
down=missing.max(desired);
}{
down=up;
}
};
expand/*.debug("exporting")*/;
}.flatten;
semitones=(semitones-this.gtranspose).mod(12).sort;
// making a new event, based on the original one, and replacing its scale by the new one
^this.composeEvents((scale: semitones));
}
playNotes { |semitones, verbose=false|
var sign=(if(this.isMinor){$m} {$M});//.debug("Major/Minor");
var idx=this.gtranspose.round(1.0).mod(12);
var root=(ChordAnalyzer.roots.at(sign)[idx]);//.debug("Root");
// (ChordAnalyzer.roots.at($#)[idx]).debug("As #");
// (ChordAnalyzer.roots.at($b)[idx]).debug("As b");
if(this.isMinor)
{
sign=(if((ChordAnalyzer.roots.at($b)[idx])==root) {$b} {$#});//.debug("#/b");
}
{
sign=(if((ChordAnalyzer.roots.at($#)[idx])==root) {$#} {$m});//.debug("#/b");
};
{
(semitones?this.note).do{
|note|
var e=(scale: this.scale, gtranspose: this.gtranspose);
e.note=note;
e.play;
if(verbose) {
(note: note, gtranspose: this.gtranspose).notename(sign).postln;
};
1.wait;
}
}.fork
}
playScaleNotes { |verbose=false|
var semitones=if(this.scale.respondsTo(\semitones)) { this.scale.semitones}{this.scale};
this.playNotes(semitones, verbose);//.postln;
}
notename { |sign|
var notes;
if (this.note.isNil) { Exception("Note not found in Event").throw;};
if (sign.isNil) {sign=this.signForScale?$#};
notes=this.note.asArray;
^notes.collect{ |note|
var notename, r;
note=note.asInteger;
r=note+this.gtranspose;
r=r.mod(12);
notename=ChordAnalyzer.roots.at(sign)[r];
//notename.postln;
}
}
scalenotes {
var tmp, sign, work;
if (this.scale.isNil.and(this.name.isNil)) { Exception("Scale or (Chord)Name not found in Event").throw;};
// if no scale but a name, we extract the scale from the name
tmp=if(this.scale.notNil){this}{this.name.asChordEvent};
sign=tmp.signForScale;
work=(note: if(tmp.scale.respondsTo(\semitones)) { tmp.scale.semitones}{tmp.scale}, gtranspose: tmp.gtranspose);
^work.notename(sign);
}
signForScale {
var g=this.gtranspose.mod(12);
var m=this.isMinor;
var name;
var sign;
name=if(this.rootname.notNil) {
this.rootname/*.debug("using rootname from event")*/;
}{
if (this.name.notNil) {
try{
var tmp=this.name.asChordEvent;
tmp.rootname/*.debug("using rootname from chordname")*/;
} { |err|
"Cannot extract a rootname from the chord's name".warn;
nil;
}
}
};
sign=if (name.notNil) {
// If we have a chord name, using the #/b provided in the name
if (ChordAnalyzer.roots[$b][g]==name) {$b} {$#};
}{
ChordAnalyzer.defaults[m.if($m,$M)][g];
};
^sign;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment