This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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